diff --git a/src/app/components/ApplyNow/ApplyNow.scss b/src/app/components/ApplyNow/ApplyNow.scss new file mode 100644 index 0000000..ad83257 --- /dev/null +++ b/src/app/components/ApplyNow/ApplyNow.scss @@ -0,0 +1,20 @@ +.apply-now-svg { + align-self: flex-end; + margin-inline: auto; + opacity: 0; + font-size: $p-font; + display: flex; + + .mask { + fill: none; + stroke: #fff; + stroke-dasharray: 0; + stroke-dashoffset: 0; + } + + @keyframes dash { + to { + stroke-dashoffset: 0; + } + } +} diff --git a/src/app/components/ApplyNow/ApplyNow.tsx b/src/app/components/ApplyNow/ApplyNow.tsx new file mode 100644 index 0000000..186f3d2 --- /dev/null +++ b/src/app/components/ApplyNow/ApplyNow.tsx @@ -0,0 +1,59 @@ +import { RefObject } from 'react'; +import './ApplyNow.scss'; +import ApplyNowSVG from './ApplyNowSVG'; +// Credit to: https://css-tricks.com/how-to-get-handwriting-animation-with-irregular-svg-strokes/ + +type ApplyNowTypes = { + applySVGRef: RefObject; +}; + +export default function ApplyNow({ applySVGRef }: ApplyNowTypes) { + return ( + <> + {/* prettier-ignore */} + + Apply Now! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/components/ApplyNow/ApplyNowSVG.tsx b/src/app/components/ApplyNow/ApplyNowSVG.tsx new file mode 100644 index 0000000..d5ca4d2 --- /dev/null +++ b/src/app/components/ApplyNow/ApplyNowSVG.tsx @@ -0,0 +1,9 @@ +export default function ApplyNowSVG() { + return ( + + ); +} diff --git a/src/app/components/FutureUpgrader/FutureUpgrader.scss b/src/app/components/FutureUpgrader/FutureUpgrader.scss new file mode 100644 index 0000000..e2b15c8 --- /dev/null +++ b/src/app/components/FutureUpgrader/FutureUpgrader.scss @@ -0,0 +1,20 @@ +.future-upgrader-svg { + align-self: flex-end; + margin-inline: auto; + opacity: 0; + font-size: $p-font; + display: flex; + + .mask { + fill: none; + stroke: #fff; + stroke-dasharray: 0; + stroke-dashoffset: 0; + } + + @keyframes dash { + to { + stroke-dashoffset: 0; + } + } +} diff --git a/src/app/components/FutureUpgrader/FutureUpgrader.tsx b/src/app/components/FutureUpgrader/FutureUpgrader.tsx new file mode 100644 index 0000000..d702aba --- /dev/null +++ b/src/app/components/FutureUpgrader/FutureUpgrader.tsx @@ -0,0 +1,77 @@ +import { RefObject } from 'react'; +import './FutureUpgrader.scss'; +import FutureUpgraderSVG from './FutureUpgraderSVG'; +// Credit to: https://css-tricks.com/how-to-get-handwriting-animation-with-irregular-svg-strokes/ + +type FutureUpgraderTypes = { + futureSVGRef: RefObject; +}; + +export default function FutureUpgrader({ futureSVGRef }: FutureUpgraderTypes) { + return ( + <> + {/* prettier-ignore */} + + Future Upgrader + + + {/* Future */} + + + + + + + + + + + + + + + + + + + + + + {/* Upgrader */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/app/components/FutureUpgrader/FutureUpgraderSVG.tsx b/src/app/components/FutureUpgrader/FutureUpgraderSVG.tsx new file mode 100644 index 0000000..6747d91 --- /dev/null +++ b/src/app/components/FutureUpgrader/FutureUpgraderSVG.tsx @@ -0,0 +1,9 @@ +export default function FutureUpgraderSVG() { + return ( + + ); +} diff --git a/src/app/components/Navbar/Hamburger/Hamburger.scss b/src/app/components/Navbar/Hamburger/Hamburger.scss index 4684ac2..0b0a55e 100644 --- a/src/app/components/Navbar/Hamburger/Hamburger.scss +++ b/src/app/components/Navbar/Hamburger/Hamburger.scss @@ -1,11 +1,12 @@ .hamburger { - $hamburger-height: 18px; - $hamburger-width: 30px; + $hamburger-height: 16px; + $hamburger-width: 25px; $line-height: 4px; position: absolute; - margin: $main-padding-mobile-block; - right: 0; + margin-block: $main-padding-mobile-block; + margin-inline: $main-padding-mobile-inline; + left: 0; background: none; outline-offset: 3px; border: none; @@ -18,7 +19,8 @@ z-index: 3; @media (min-width: $mobile-breakpoint) { - margin: $main-padding-desktop-block; + margin-block: $main-padding-desktop-block; + margin-inline: $main-padding-desktop-inline; } .line { diff --git a/src/app/components/Navbar/Navbar.scss b/src/app/components/Navbar/Navbar.scss index e83bede..d91b80f 100644 --- a/src/app/components/Navbar/Navbar.scss +++ b/src/app/components/Navbar/Navbar.scss @@ -2,20 +2,39 @@ nav { a { text-decoration: none; } + + h1 { + padding-bottom: 7px; + @media (min-width: $tablet-breakpoint) { + font-size: clamp(22px, calc(20px + 0.6vw), 34px); + } + } + + h2 { + font-size: 20px; + font-weight: 400px; + } + + h3 { + font-size: 18px; + } + .nav-container { padding-inline: $main-padding-mobile-inline; padding-block: $main-padding-mobile-block; width: 100vw; - max-width: 500px; - background: rgb(165, 237, 255); + max-width: 400px; + background: rgb(255, 255, 255); height: 100dvh; display: flex; flex-direction: column; - gap: 2rem; - transition: 0.3s ease-out; position: fixed; z-index: 1; + &.slideTransition { + transition: transform 0.3s ease-out; + } + &.panel-open { transform: translateX(0%); } @@ -24,12 +43,18 @@ nav { transform: translateX(-100%); } + @media (max-width: $mobile-breakpoint) { + padding-inline: clamp(34px, 12px + 1.2vw, 40px); + padding-top: 5rem; + } + @media (min-width: $mobile-breakpoint) { padding-inline: $main-padding-desktop-inline; padding-block: $main-padding-desktop-block; } @media (min-width: $tablet-breakpoint) { + max-width: 500px; width: $nav-width; visibility: visible; position: static; @@ -39,10 +64,20 @@ nav { } } + .event-logistics { + margin-bottom: 1rem; + + .event-date { + padding-top: 7px; + font-size: clamp(14px, calc(12px + 0.55vw), 20px); + } + } + ul { list-style: none; font-family: $p-font; padding-left: 0; + margin-block: 2rem; } } @@ -62,4 +97,100 @@ nav { opacity: 0.25; } } + + .nav-link-list { + display: grid; + gap: 14px; + + .nav-link { + display: flex; + + .nav-link-text { + position: relative; + + &:before { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background-color: #212121; + transition: width 0.25s; + } + + &:is(:hover, :focus-visible):before { + width: 100%; + left: 0; + } + } + } + } + + .highlight { + position: absolute; + left: 10; + background-color: #f5ff85; + } + + .asterisk { + word-wrap: break-word; + min-height: 1em; + height: 1em; + overflow-y: hidden; + color: #bababa; + font-family: monospace; + } + + .nav-footer { + margin-top: auto; + display: flex; + justify-content: center; + flex-direction: column; + } + + .apply-now { + margin-bottom: 40px; + + @media (max-width: $tablet-breakpoint) { + display: none; + } + } + + .add-me-to-cart { + max-width: 310px; + color: #fcfcfc; + background-color: #212121; + padding-block: 20px; + transition: background-color 0.3s; + border: 2px solid $primary-black; + border-radius: 5px; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + gap: clamp(12px, 1vw, 22px); + + @media (min-width: $mobile-breakpoint) { + margin-bottom: clamp(15px, calc(10px + 1.2vw), 40px); + } + + &:hover { + background-color: #fcfcfc; + color: #212121; + } + } + + .thank-you-container { + display: none; + + @media (min-width: $mobile-breakpoint) { + display: block; + } + .thank-you-label { + @media (min-width: $tablet-breakpoint) { + font-size: clamp(12px, calc(7px + 0.62vw), $p-size); + } + } + } } diff --git a/src/app/components/Navbar/Navbar.tsx b/src/app/components/Navbar/Navbar.tsx index cb27549..d7f231d 100644 --- a/src/app/components/Navbar/Navbar.tsx +++ b/src/app/components/Navbar/Navbar.tsx @@ -1,49 +1,49 @@ 'use client'; import Link from 'next/link'; import './Navbar.scss'; -import React, { useState } from 'react'; +import { useState, useRef, RefObject } from 'react'; import Hamburger from './Hamburger/Hamburger'; import FocusTrap from 'focus-trap-react'; +import { useHandleHamburger } from './hooks/useHandleHamburger'; +import { useHandleScroll } from './hooks/useHandleScroll'; +import Signature from '../Signature/Signature'; -export default function Navbar() { - const hamburgerInnerRef = React.useRef(null); - const [isHamburgerOpen, setIsHamburgerOpen] = useState(false); - const links = [ - { href: '#landing', text: 'Home' }, - { href: '#overview', text: 'Overview' }, - { href: '#themes', text: 'Themes' }, - { href: '#faq', text: 'FAQ' }, - { href: '#apply', text: 'Apply' } - ]; +type NavbarProps = { + pageRefs: { + mainRef: RefObject; + landingRef: RefObject; + overviewRef: RefObject; + themesRef: RefObject; + faqRef: RefObject; + applyRef: RefObject; + }; +}; - const toggleHamburger = () => { - setIsHamburgerOpen(!isHamburgerOpen); +const NAV_LINKS = [ + { href: '#landing', text: 'Home' }, + { href: '#overview', text: 'Overview' }, + { href: '#themes', text: 'Themes' }, + { href: '#faq', text: 'FAQ' }, + { href: '#apply', text: 'Apply' } +]; - if (!isHamburgerOpen) { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') { - setIsHamburgerOpen(false); - document.body.style.overflow = 'auto'; - document.removeEventListener('keydown', handleKeyDown); - } - }; - document.querySelector('main')?.setAttribute('aria-hidden', 'true'); - document.addEventListener('keydown', handleKeyDown); - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'auto'; - document.querySelector('main')?.removeAttribute('aria-hidden'); - // Add transition to hamburger menu background color when closing navbar - if (hamburgerInnerRef.current) { - hamburgerInnerRef.current.style.transition = - 'transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), background-color 0.3s'; - hamburgerInnerRef.current.addEventListener('animationend', () => { - hamburgerInnerRef.current!.style.transition = - 'transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19)'; - }); - } - } - }; +// Overshoot number of asterisks, as it will anything past first line will be hidden +const NUM_ASTERISKS = 50; + +const PAGE_TYPES = ['Home', 'Overview', 'Themes', 'FAQ', 'Apply'] as const; +export type PageType = (typeof PAGE_TYPES)[number]; + +export default function Navbar({ pageRefs }: NavbarProps) { + const navContainerRef = useRef(null); + const asterisksRef1 = useRef(null); + const asterisksRef2 = useRef(null); + const [currPage, setCurrPage] = useState('Home'); + const [hoveringCart, setHoveringCart] = useState(false); + + const { toggleHamburger, isHamburgerOpen, hamburgerInnerRef } = + useHandleHamburger({ navContainerRef }); + + useHandleScroll({ setCurrPage, pageRefs, PAGE_TYPES }); return ( @@ -55,26 +55,121 @@ export default function Navbar() { />

UP-GRADE 2024

-
; +}; + +export const useHandleHamburger = ({ + navContainerRef +}: UseHandleHamburgerProps) => { + const hamburgerInnerRef = useRef(null); + const [isHamburgerOpen, setIsHamburgerOpen] = useState(false); + + const toggleHamburger = () => { + // Toggle slide transition to account for mobile to desktop resize + navContainerRef.current?.classList.toggle('slideTransition'); + setIsHamburgerOpen(!isHamburgerOpen); + + if (!isHamburgerOpen) { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + setIsHamburgerOpen(false); + document.body.style.overflow = 'auto'; + document.removeEventListener('keydown', handleKeyDown); + } + }; + document.querySelector('main')?.setAttribute('aria-hidden', 'true'); + document.addEventListener('keydown', handleKeyDown); + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'auto'; + document.querySelector('main')?.removeAttribute('aria-hidden'); + // Add transition to hamburger menu background color when closing navbar + if (hamburgerInnerRef.current) { + hamburgerInnerRef.current.style.transition = + 'transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), background-color 0.3s'; + hamburgerInnerRef.current.addEventListener('animationend', () => { + hamburgerInnerRef.current!.style.transition = + 'transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19)'; + }); + } + } + }; + + return { toggleHamburger, isHamburgerOpen, hamburgerInnerRef }; +}; diff --git a/src/app/components/Navbar/hooks/useHandleScroll.ts b/src/app/components/Navbar/hooks/useHandleScroll.ts new file mode 100644 index 0000000..8b7d743 --- /dev/null +++ b/src/app/components/Navbar/hooks/useHandleScroll.ts @@ -0,0 +1,81 @@ +import { Dispatch, RefObject, SetStateAction, useEffect } from 'react'; +import { PageType } from '../Navbar'; + +type UseHandleScrollType = { + setCurrPage: Dispatch>; + pageRefs: { + mainRef: RefObject; + landingRef: RefObject; + overviewRef: RefObject; + themesRef: RefObject; + faqRef: RefObject; + applyRef: RefObject; + }; + PAGE_TYPES: readonly ['Home', 'Overview', 'Themes', 'FAQ', 'Apply']; +}; + +export const useHandleScroll = ({ + setCurrPage, + pageRefs, + PAGE_TYPES +}: UseHandleScrollType) => { + const { mainRef, landingRef, overviewRef, themesRef, faqRef, applyRef } = + pageRefs; + useEffect(() => { + const mainElement = mainRef.current; + const landingElement = landingRef.current; + const overviewElement = overviewRef.current; + const themesElement = themesRef.current; + const faqElement = faqRef.current; + const applyElement = applyRef.current; + + /** + * Sets the current page + * Current page is based on what part of scroll we are at + */ + if ( + !mainElement || + !landingElement || + !overviewElement || + !themesElement || + !faqElement || + !applyElement + ) + return; + + const pagesList = [ + landingElement, + overviewElement, + themesElement, + faqElement, + applyElement + ]; + const handleScroll = () => { + const scrollPosition = mainElement.scrollTop || 0; + + pagesList.forEach((page, index) => { + const pageTop = page.offsetTop; + const pageBottom = pageTop + page.clientHeight; + + const halfScrollPosition = scrollPosition + window.innerHeight / 2; + if (halfScrollPosition > pageTop && halfScrollPosition < pageBottom) { + setCurrPage(PAGE_TYPES[index]); + } + }); + }; + + mainElement.addEventListener('scroll', handleScroll); + return () => { + mainElement.removeEventListener('scroll', handleScroll); + }; + }, [ + PAGE_TYPES, + applyRef, + faqRef, + landingRef, + mainRef, + overviewRef, + setCurrPage, + themesRef + ]); +}; diff --git a/src/app/components/Signature/Signature.scss b/src/app/components/Signature/Signature.scss new file mode 100644 index 0000000..3635e5d --- /dev/null +++ b/src/app/components/Signature/Signature.scss @@ -0,0 +1,34 @@ +.signature { + display: none; + + @media (min-width: $mobile-breakpoint) { + display: flex; + } + + position: relative; + margin-top: 10px; + margin-bottom: clamp(15px, calc(3px + 1vw), 40px); + font-size: $p-font; + height: 60px; + + .signature-container { + display: flex; + align-items: flex-end; + position: absolute; + bottom: 15px; + width: 100%; + gap: 12px; + + span { + font-family: $p-font; + line-height: 1; + } + + .line { + width: 100%; + height: 1px; + background-color: #bababa; + z-index: -1; + } + } +} diff --git a/src/app/components/Signature/Signature.tsx b/src/app/components/Signature/Signature.tsx new file mode 100644 index 0000000..a7cdd7d --- /dev/null +++ b/src/app/components/Signature/Signature.tsx @@ -0,0 +1,69 @@ +'use client'; +import { RefObject, useEffect, useRef } from 'react'; +import ApplyNow from '../ApplyNow/ApplyNow'; +import FutureUpgrader from '../FutureUpgrader/FutureUpgrader'; +import './Signature.scss'; +import { handleHandwritingAnimation } from './util/handleHandwritingAnimation'; + +type SignatureProps = { + hoveringCart: boolean; + navContainerRef: RefObject; +}; + +const APPLY_CONFIG = { + maskPrefix: 'apply', + animationDuration: 0.055, + maskList: ['A', 'p-1', 'p-2', 'l', 'y', 'n', 'o', 'w', 'exclamation'], + heightOffset: '0%' +}; + +const FUTURE_CONFIG = { + maskPrefix: 'future', + animationDuration: 0.05, + // prettier-ignore + maskList: ['F', 'u1', 't', 'u2', 'r1', 'e1', 'U', 'p', 'dash', 'g', 'r2', 'a', 'd', 'e2', 'r3'], + heightOffset: '15%' +}; + +export default function Signature({ + hoveringCart, + navContainerRef +}: SignatureProps) { + const xElementRef = useRef(null); + const applySVGRef = useRef(null); + const futureSVGRef = useRef(null); + + useEffect(() => { + if (!hoveringCart) { + console.log('APPLY NOW ANIMATION!'); + handleHandwritingAnimation({ + svgRef: applySVGRef, + navContainerRef, + xElementRef, + config: APPLY_CONFIG + }); + } else { + console.log('FUTURE UPGRADER ANIMATION!'); + handleHandwritingAnimation({ + svgRef: futureSVGRef, + navContainerRef, + xElementRef, + config: FUTURE_CONFIG + }); + } + }, [hoveringCart, navContainerRef]); + + return ( +
+
+ X +
+
+ {!hoveringCart ? ( + + ) : ( + + )} +
+ ); +} diff --git a/src/app/components/Signature/util/handleHandwritingAnimation.ts b/src/app/components/Signature/util/handleHandwritingAnimation.ts new file mode 100644 index 0000000..578694d --- /dev/null +++ b/src/app/components/Signature/util/handleHandwritingAnimation.ts @@ -0,0 +1,52 @@ +import { RefObject } from 'react'; + +type HandleHandwritingAnimationProps = { + svgRef: RefObject; + navContainerRef: RefObject; + xElementRef: RefObject; + config: { + maskPrefix: string; + animationDuration: number; + maskList: string[]; + heightOffset: string; + }; +}; + +export const handleHandwritingAnimation = ({ + svgRef, + navContainerRef, + xElementRef, + config +}: HandleHandwritingAnimationProps) => { + const { animationDuration, maskList, maskPrefix, heightOffset } = config; + const applyContainer = svgRef.current; + const navContainer = navContainerRef.current; + const xElement = xElementRef.current; + if (!applyContainer || !navContainer || !xElement) return; + const navFullWidth = parseFloat(window.getComputedStyle(navContainer).width); + const navPadding = parseFloat( + window.getComputedStyle(navContainer).paddingLeft + ); + const navWidth = navFullWidth - navPadding * 2; + const xWidth = parseFloat(window.getComputedStyle(xElement).width); + const maxWidth = Math.min(navWidth - xWidth - 20, 230); + + svgRef.current.style.width = `${maxWidth}px`; + svgRef.current.style.transform = `translateX(${xWidth}px) translateY(${heightOffset})`; + applyContainer.style.opacity = '1'; + let currentDelay = 0; + for (let maskID of maskList) { + const maskElement = document.querySelector(`#${maskPrefix}-mask-${maskID}`); + console.log(maskElement); + if (!maskElement) return; + const maskChildren = maskElement.children; + for (var i = 0; i < maskChildren.length; i++) { + const pathChild = maskChildren[i] as SVGPathElement; + const length = pathChild.getTotalLength(); + pathChild.style.strokeDasharray = length.toString(); + pathChild.style.strokeDashoffset = length.toString(); + pathChild.style.animation = `dash ${animationDuration}s ${i * animationDuration + currentDelay}s linear forwards`; + } + currentDelay += maskChildren.length * animationDuration; + } +}; diff --git a/src/app/page.tsx b/src/app/page.tsx index 6f4ee59..6bf7e1c 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,3 +1,6 @@ +'use client'; + +import React, { useRef } from 'react'; import './Home.scss'; import Landing from './pages/Landing/Landing'; @@ -7,17 +10,35 @@ import FAQ from './pages/FAQ/FAQ'; import Apply from './pages/Apply/Apply'; import Navbar from './components/Navbar/Navbar'; -export default async function Home() { +export type PageRef = React.RefObject; + +export default function Home() { + const mainRef = useRef(null); + const landingRef = useRef(null); + const overviewRef = useRef(null); + const themesRef = useRef(null); + const faqRef = useRef(null); + const applyRef = useRef(null); + return (
- -
- - - - + +
+ + + + {/* Footer is inside Apply */} - +
); diff --git a/src/app/pages/Apply/Apply.tsx b/src/app/pages/Apply/Apply.tsx index fed31f5..3869e1f 100644 --- a/src/app/pages/Apply/Apply.tsx +++ b/src/app/pages/Apply/Apply.tsx @@ -1,9 +1,14 @@ import Footer from '@/components/Footer/Footer'; import './Apply.scss'; +import { PageRef } from '@/page'; -export default function Apply() { +type ApplyProps = { + applyRef: PageRef; +}; + +export default function Apply({ applyRef }: ApplyProps) { return ( -
+

.05 / Apply

Why Apply?