diff --git a/public/assets/graphics/onboarding/Fall23Allocation-JustinLu.jpg b/public/assets/graphics/onboarding/Fall23Allocation-JustinLu.jpg new file mode 100644 index 00000000..01c0712b Binary files /dev/null and b/public/assets/graphics/onboarding/Fall23Allocation-JustinLu.jpg differ diff --git a/public/assets/graphics/onboarding/Fall24BitByteInfo_1-JustinLu.jpg b/public/assets/graphics/onboarding/Fall24BitByteInfo_1-JustinLu.jpg new file mode 100644 index 00000000..b35a13c8 Binary files /dev/null and b/public/assets/graphics/onboarding/Fall24BitByteInfo_1-JustinLu.jpg differ diff --git a/public/assets/graphics/onboarding/Fall24BitByteInfo_2-JustinLu.jpg b/public/assets/graphics/onboarding/Fall24BitByteInfo_2-JustinLu.jpg new file mode 100644 index 00000000..8173672f Binary files /dev/null and b/public/assets/graphics/onboarding/Fall24BitByteInfo_2-JustinLu.jpg differ diff --git a/public/assets/graphics/onboarding/Fall24Bonfire_1-JustinLu.jpg b/public/assets/graphics/onboarding/Fall24Bonfire_1-JustinLu.jpg new file mode 100644 index 00000000..3957c12d Binary files /dev/null and b/public/assets/graphics/onboarding/Fall24Bonfire_1-JustinLu.jpg differ diff --git a/public/assets/graphics/onboarding/Fall24Bonfire_2-JustinLu.jpg b/public/assets/graphics/onboarding/Fall24Bonfire_2-JustinLu.jpg new file mode 100644 index 00000000..38e14fab Binary files /dev/null and b/public/assets/graphics/onboarding/Fall24Bonfire_2-JustinLu.jpg differ diff --git a/public/assets/graphics/onboarding/Fall24Kickoff_1-JustinLu.jpg b/public/assets/graphics/onboarding/Fall24Kickoff_1-JustinLu.jpg new file mode 100644 index 00000000..33f47568 Binary files /dev/null and b/public/assets/graphics/onboarding/Fall24Kickoff_1-JustinLu.jpg differ diff --git a/public/assets/graphics/onboarding/Fall24Kickoff_2-JustinLu.jpg b/public/assets/graphics/onboarding/Fall24Kickoff_2-JustinLu.jpg new file mode 100644 index 00000000..a7858f20 Binary files /dev/null and b/public/assets/graphics/onboarding/Fall24Kickoff_2-JustinLu.jpg differ diff --git a/public/assets/graphics/onboarding/Fall24Kickoff_3-JustinLu.jpg b/public/assets/graphics/onboarding/Fall24Kickoff_3-JustinLu.jpg new file mode 100644 index 00000000..bbe2176e Binary files /dev/null and b/public/assets/graphics/onboarding/Fall24Kickoff_3-JustinLu.jpg differ diff --git a/src/components/onboarding/Intro/index.tsx b/src/components/onboarding/Intro/index.tsx index 4d8dc962..d9c67c06 100644 --- a/src/components/onboarding/Intro/index.tsx +++ b/src/components/onboarding/Intro/index.tsx @@ -1,27 +1,165 @@ -import TempImage from '@/public/assets/graphics/cat404.png'; -import Image from 'next/image'; +import BitByteAllocation from '@/public/assets/graphics/onboarding/Fall23Allocation-JustinLu.jpg'; +import BitByteInfoSession from '@/public/assets/graphics/onboarding/Fall24BitByteInfo_1-JustinLu.jpg'; +import BitByteSpeedFriending from '@/public/assets/graphics/onboarding/Fall24BitByteInfo_2-JustinLu.jpg'; +import BonfireMarshmellows from '@/public/assets/graphics/onboarding/Fall24Bonfire_1-JustinLu.jpg'; +import BonfireBeach from '@/public/assets/graphics/onboarding/Fall24Bonfire_2-JustinLu.jpg'; +import KickoffSideView from '@/public/assets/graphics/onboarding/Fall24Kickoff_1-JustinLu.jpg'; +import KickoffCloseUp from '@/public/assets/graphics/onboarding/Fall24Kickoff_2-JustinLu.jpg'; +import KickoffFrontView from '@/public/assets/graphics/onboarding/Fall24Kickoff_3-JustinLu.jpg'; +import Image, { StaticImageData } from 'next/image'; +import { useEffect, useRef, useState } from 'react'; import styles from './style.module.scss'; +type ImageBounds = { + x: number; + y: number; + width: number; + height: number; +}; + +function getCenter({ x, y, width, height }: ImageBounds): { x: number; y: number } { + return { x: x + width / 2, y: y + height / 2 }; +} + +type IntroImage = { + src: StaticImageData; + alt: string; + desktopSize: ImageBounds; + mobileSize: ImageBounds; + round?: boolean; +}; + +// Measurements from +// https://www.figma.com/design/GiWZdbzJ2uxyknpCrB9UK6/acm-onboarding, ordered +// from bottom to top (reversed from Figma, where layers are ordered top to +// bottom) +const firstImage = { + src: KickoffFrontView, + alt: 'A large audience of students forming diamonds with their hands', + desktopSize: { x: 173, y: 116, width: 631, height: 308 }, + mobileSize: { x: 12, y: 173, width: 287, height: 206 }, + round: true, +}; +const images: IntroImage[] = [ + firstImage, + { + src: BonfireBeach, + alt: 'Students standing on a beach on a cloudy day', + desktopSize: { x: 289, y: 29, width: 326, height: 146 }, + mobileSize: { x: 0, y: 116, width: 146, height: 110 }, + }, + { + src: BitByteAllocation, + alt: 'A group photo of students standing before Geisel and Snake Path', + desktopSize: { x: 57, y: 357, width: 299, height: 135 }, + mobileSize: { x: 0, y: 422, width: 270, height: 131 }, + }, + { + src: KickoffSideView, + alt: 'A large audience of students at ACM Kickoff', + desktopSize: { x: 327, y: 390, width: 393, height: 134 }, + mobileSize: { x: 0, y: 0, width: 252, height: 108 }, + round: true, + }, + { + src: BitByteInfoSession, + alt: 'An audience of students watching a presentation', + desktopSize: { x: 685, y: 72, width: 233, height: 135 }, + mobileSize: { x: 124, y: 369, width: 187, height: 84 }, + }, + { + src: BitByteSpeedFriending, + alt: 'Students chatting', + desktopSize: { x: 49, y: 175, width: 138, height: 138 }, + mobileSize: { x: 213, y: 293, width: 86, height: 86 }, + round: true, + }, + { + src: BonfireMarshmellows, + alt: 'Students around a bonfire holding marshmellows on skewers', + desktopSize: { x: 740, y: 285, width: 208, height: 208 }, + mobileSize: { x: 158, y: 86, width: 153, height: 153 }, + round: true, + }, + { + src: KickoffCloseUp, + alt: 'A student smiling in a crowd', + desktopSize: { x: 89, y: 46, width: 168, height: 168 }, + mobileSize: { x: 24, y: 326, width: 109, height: 109 }, + round: true, + }, +]; +// The center of the coordinate system +const DESKTOP_OFFSET = getCenter(firstImage.desktopSize); +const MOBILE_OFFSET = getCenter(firstImage.mobileSize); + +const displayImage = ( + mode: 'desktop' | 'mobile', + { src, alt, round, ...sizes }: IntroImage, + index: number +) => { + const { x, y, width, height } = sizes[`${mode}Size`]; + const offset = mode === 'desktop' ? DESKTOP_OFFSET : MOBILE_OFFSET; + return ( + {alt} + ); +}; + const Intro = () => { + const [mouseX, setMouseX] = useState(0); + const [mouseY, setMouseY] = useState(0); + const ref = useRef(null); + + useEffect(() => { + const handleMouseMove = (e: PointerEvent) => { + if (!ref.current) { + return; + } + const { left, top } = ref.current.getBoundingClientRect(); + setMouseX(e.pointerType === 'mouse' ? e.clientX - left : 0); + setMouseY(e.pointerType === 'mouse' ? e.clientY - top : 0); + }; + document.addEventListener('pointermove', handleMouseMove); + return () => { + document.removeEventListener('pointermove', handleMouseMove); + }; + }); + return (
- temp image - temp image - temp image +
+ {images.map((image, i) => ( +
+ {displayImage('desktop', image, i)} + {displayImage('mobile', image, i)} +
+ ))} +
); }; diff --git a/src/components/onboarding/Intro/style.module.scss b/src/components/onboarding/Intro/style.module.scss index f7e3483a..821f24a5 100644 --- a/src/components/onboarding/Intro/style.module.scss +++ b/src/components/onboarding/Intro/style.module.scss @@ -4,7 +4,7 @@ from { filter: blur(2rem); opacity: 0; - transform: scale(0.5); + transform: scale(1.2); } to { @@ -18,16 +18,43 @@ align-items: center; display: flex; flex: auto; - gap: 1rem; justify-content: center; - - .image { - animation: appear 1.5s backwards; + min-height: 30rem; + @media (max-width: vars.$breakpoint-lg) { + margin: 0 -2rem; + min-height: 35rem; + overflow: hidden; } - @media (max-width: vars.$breakpoint-md) { - .desktopOnly { - display: none; + .anchor { + position: relative; + + .imageWrapper { + .image { + animation: appear 1s backwards; + border-radius: 1rem; + object-fit: cover; + object-position: center; + position: absolute; + + &.pill { + border-radius: 10rem; + } + + &.mobileOnly { + display: none; + } + + @media (max-width: vars.$breakpoint-lg) { + &.mobileOnly { + display: block; + } + + &.desktopOnly { + display: none; + } + } + } } } } diff --git a/src/components/onboarding/Intro/style.module.scss.d.ts b/src/components/onboarding/Intro/style.module.scss.d.ts index 69bc1b04..28caa9ca 100644 --- a/src/components/onboarding/Intro/style.module.scss.d.ts +++ b/src/components/onboarding/Intro/style.module.scss.d.ts @@ -1,7 +1,11 @@ export type Styles = { + anchor: string; appear: string; desktopOnly: string; image: string; + imageWrapper: string; + mobileOnly: string; + pill: string; wrapper: string; }; diff --git a/src/components/onboarding/Leaderboard/index.tsx b/src/components/onboarding/Leaderboard/index.tsx index ea296981..0904f90d 100644 --- a/src/components/onboarding/Leaderboard/index.tsx +++ b/src/components/onboarding/Leaderboard/index.tsx @@ -73,7 +73,10 @@ const Leaderboard = ({ user }: LeaderboardProps) => { }, [userPoints]); return ( -
+
{users.map(({ name, points, image }) => { const position = sorted.indexOf(name); return ( @@ -87,7 +90,7 @@ const Leaderboard = ({ user }: LeaderboardProps) => { even={name !== userName} className={styles.row} style={{ - transform: `translateY(${position * 4}rem)`, + transform: `translateY(calc(${position} * var(--leaderboard-height)))`, zIndex: name === userName ? '5' : undefined, }} /> diff --git a/src/components/onboarding/Leaderboard/style.module.scss b/src/components/onboarding/Leaderboard/style.module.scss index 6aed2179..33d1aa75 100644 --- a/src/components/onboarding/Leaderboard/style.module.scss +++ b/src/components/onboarding/Leaderboard/style.module.scss @@ -1,15 +1,27 @@ @use 'src/styles/vars.scss' as vars; .wrapper { + --leaderboard-height: 4rem; border-radius: 0.5rem; overflow: hidden; position: relative; + @media (max-width: vars.$breakpoint-md) { + --leaderboard-height: 5rem; + } + .row { + height: var(--leaderboard-height); left: 0; position: absolute; right: 0; top: 0; transition: transform 0.2s; + + @media (max-width: vars.$breakpoint-sm) { + img { + display: none; + } + } } } diff --git a/src/lib/types/apiRequests.ts b/src/lib/types/apiRequests.ts index a62efc8b..11a4a80d 100644 --- a/src/lib/types/apiRequests.ts +++ b/src/lib/types/apiRequests.ts @@ -77,6 +77,7 @@ export interface UserPatches { graduationYear?: number; bio?: string; isAttendancePublic?: boolean; + onboardingSeen?: boolean; passwordChange?: PasswordUpdate; } diff --git a/src/lib/types/apiResponses.ts b/src/lib/types/apiResponses.ts index 7d822f29..a434d9ec 100644 --- a/src/lib/types/apiResponses.ts +++ b/src/lib/types/apiResponses.ts @@ -382,6 +382,7 @@ export interface PrivateProfile extends PublicProfile { state: UserState; credits: number; resumes?: PublicResume[]; + onboardingSeen: boolean; } export interface PublicFeedback { diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 664c6c6c..0a5bdb84 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -97,13 +97,7 @@ const PortalHomePage = ({ setCheckinModalVisible(false); // Start onboarding after checking in - // TEMP: This should be saved server-side in the future - // Do not start onboarding if user already attended other events - if (attendance.length > 1) { - return; - } - const onboardingState = localStorage.getItem(config.tempLocalOnboardingKey); - if (onboardingState === 'onboarded') { + if (user.onboardingSeen) { return; } router.push( diff --git a/src/pages/onboard.tsx b/src/pages/onboard.tsx index 807074b4..92077e04 100644 --- a/src/pages/onboard.tsx +++ b/src/pages/onboard.tsx @@ -1,5 +1,6 @@ import { OnboardingScreen } from '@/components/onboarding'; import { config } from '@/lib'; +import { UserAPI } from '@/lib/api'; import withAccessType, { GetServerSidePropsWithAuth } from '@/lib/hoc/withAccessType'; import { PermissionService } from '@/lib/services'; import { URL } from '@/lib/types'; @@ -9,10 +10,11 @@ import { useRouter } from 'next/router'; interface OnboardProps { destination: URL; + authToken: string; user: PrivateProfile; } -const OnboardPage: NextPage = ({ user, destination }) => { +const OnboardPage: NextPage = ({ authToken, user, destination }) => { const router = useRouter(); const handleExit = () => { @@ -25,7 +27,7 @@ const OnboardPage: NextPage = ({ user, destination }) => { user={user} onDismiss={handleExit} onFinish={() => { - localStorage.setItem(config.tempLocalOnboardingKey, 'onboarded'); + UserAPI.updateCurrentUserProfile(authToken, { onboardingSeen: true }); }} />
@@ -34,12 +36,13 @@ const OnboardPage: NextPage = ({ user, destination }) => { export default OnboardPage; -const getServerSidePropsFunc: GetServerSidePropsWithAuth = async ({ query }) => { +const getServerSidePropsFunc: GetServerSidePropsWithAuth = async ({ query, authToken }) => { const route = query?.destination ? decodeURIComponent(query?.destination as string) : null; return { props: { destination: route || config.homeRoute, + authToken, quietNavbar: true, }, };