From d31fc906ae516be46b800991112feb3264436527 Mon Sep 17 00:00:00 2001 From: raymosun Date: Sun, 7 Jan 2024 19:23:48 -0800 Subject: [PATCH 01/22] Implement user profile page --- public/assets/icons/edit.svg | 3 + public/assets/icons/facebook-icon.svg | 2 +- public/assets/icons/github-icon.svg | 1 + public/assets/icons/instagram.svg | 2 +- public/assets/icons/leaderboard-icon.svg | 2 +- public/assets/icons/linkedin-icon.svg | 1 + public/assets/icons/major-icon.svg | 4 +- public/assets/icons/profile-icon.svg | 2 +- .../layout/Navbar/style.module.scss | 7 + .../profile/UserProfilePage/index.tsx | 164 ++++++++++++++ src/lib/api/EventAPI.ts | 4 +- src/lib/api/UserAPI.ts | 44 ++++ src/lib/config.ts | 5 +- src/lib/types/apiResponses.d.ts | 2 +- src/lib/utils.ts | 46 +++- src/pages/leaderboard.tsx | 4 +- src/pages/profile.tsx | 27 ++- src/pages/u/[handle].tsx | 175 ++++++++++++++- src/styles/pages/u/index.module.scss | 201 ++++++++++++++++++ src/styles/pages/u/index.module.scss.d.ts | 28 +++ 20 files changed, 684 insertions(+), 40 deletions(-) create mode 100644 public/assets/icons/edit.svg create mode 100644 public/assets/icons/github-icon.svg create mode 100644 public/assets/icons/linkedin-icon.svg create mode 100644 src/components/profile/UserProfilePage/index.tsx create mode 100644 src/styles/pages/u/index.module.scss create mode 100644 src/styles/pages/u/index.module.scss.d.ts diff --git a/public/assets/icons/edit.svg b/public/assets/icons/edit.svg new file mode 100644 index 00000000..fc3c1e6a --- /dev/null +++ b/public/assets/icons/edit.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/public/assets/icons/facebook-icon.svg b/public/assets/icons/facebook-icon.svg index d3f40bca..02c27646 100644 --- a/public/assets/icons/facebook-icon.svg +++ b/public/assets/icons/facebook-icon.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/icons/github-icon.svg b/public/assets/icons/github-icon.svg new file mode 100644 index 00000000..458be3ac --- /dev/null +++ b/public/assets/icons/github-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/instagram.svg b/public/assets/icons/instagram.svg index c0985960..2e0ecf50 100644 --- a/public/assets/icons/instagram.svg +++ b/public/assets/icons/instagram.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/public/assets/icons/leaderboard-icon.svg b/public/assets/icons/leaderboard-icon.svg index b8bdc0a4..5b3897e0 100644 --- a/public/assets/icons/leaderboard-icon.svg +++ b/public/assets/icons/leaderboard-icon.svg @@ -1,4 +1,4 @@ - + diff --git a/public/assets/icons/linkedin-icon.svg b/public/assets/icons/linkedin-icon.svg new file mode 100644 index 00000000..cb13cc42 --- /dev/null +++ b/public/assets/icons/linkedin-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/major-icon.svg b/public/assets/icons/major-icon.svg index 64e55af9..224c42df 100644 --- a/public/assets/icons/major-icon.svg +++ b/public/assets/icons/major-icon.svg @@ -1,3 +1,3 @@ - - + + diff --git a/public/assets/icons/profile-icon.svg b/public/assets/icons/profile-icon.svg index 65a36ab4..3164f29e 100644 --- a/public/assets/icons/profile-icon.svg +++ b/public/assets/icons/profile-icon.svg @@ -1,3 +1,3 @@ - + diff --git a/src/components/layout/Navbar/style.module.scss b/src/components/layout/Navbar/style.module.scss index 1bc87baf..3f9663d0 100644 --- a/src/components/layout/Navbar/style.module.scss +++ b/src/components/layout/Navbar/style.module.scss @@ -105,6 +105,8 @@ .iconLink { color: var(--theme-text-on-background-1); cursor: pointer; + height: 24px; + width: 24px; } } } @@ -150,6 +152,11 @@ &:hover { background-color: var(--theme-surface-1); } + + svg { + height: 24px; + width: 24px; + } } } diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx new file mode 100644 index 00000000..ecccaf3b --- /dev/null +++ b/src/components/profile/UserProfilePage/index.tsx @@ -0,0 +1,164 @@ +import { Carousel, Typography } from '@/components/common'; +import EventCard from '@/components/events/EventCard'; +import { config } from '@/lib'; +import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; +import { getProfilePicture, getUserRank } from '@/lib/utils'; +import DevpostIcon from '@/public/assets/icons/devpost-icon.svg'; +import EditIcon from '@/public/assets/icons/edit.svg'; +import FacebookIcon from '@/public/assets/icons/facebook-icon.svg'; +import GithubIcon from '@/public/assets/icons/github-icon.svg'; +import InstagramIcon from '@/public/assets/icons/instagram.svg'; +import LeaderboardIcon from '@/public/assets/icons/leaderboard-icon.svg'; +import LinkedinIcon from '@/public/assets/icons/linkedin-icon.svg'; +import MajorIcon from '@/public/assets/icons/major-icon.svg'; +import ProfileIcon from '@/public/assets/icons/profile-icon.svg'; +import styles from '@/styles/pages/u/index.module.scss'; +import Image from 'next/image'; +import Link from 'next/link'; +import { useEffect, useState } from 'react'; +import { AiOutlineLink } from 'react-icons/ai'; +import { IoMail } from 'react-icons/io5'; + +interface UserProfilePageProps { + user: PublicProfile; + isSignedInUser: boolean; + signedInAttendances: PublicAttendance[]; + attendances?: PublicAttendance[]; +} + +const socialMediaIcons = { + LINKEDIN: LinkedinIcon, + GITHUB: GithubIcon, + DEVPOST: DevpostIcon, + PORTFOLIO: AiOutlineLink, + FACEBOOK: FacebookIcon, + INSTAGRAM: InstagramIcon, + EMAIL: IoMail, +}; + +const UserProfilePage = ({ + user, + attendances, + signedInAttendances, + isSignedInUser, +}: UserProfilePageProps) => { + // animate the progress bar + const [progress, setProgress] = useState(0); + useEffect(() => setProgress(user.points % 100), [user.points]); + + return ( +
+
+
+
+
+ Profile Picture +
+
+ {`${user.firstName} ${user.lastName}`} + @{user.handle} +
+
+
{getUserRank(user.points)[1]}
+
+   + {user.points.toLocaleString()} Leaderboard Points +
+
+ {isSignedInUser && ( +
+ +
+ +
+ +
+ )} +
+
+
+ + {isSignedInUser ? 'My' : `${user.firstName}'s`} Progress + +
+ Level {getUserRank(user.points)[0]} + {user.points % 100}/100 +
+
+
+
+ + {isSignedInUser ? 'You need' : `${user.firstName} needs`} {100 - (user.points % 100)} more + points to level up to + +  {getUserRank(user.points + 100)[1]} + + +
+ +
+
+ About me +
+ + Class of {user.graduationYear} + + {user.major} +
+
+ {user.userSocialMedia && + Object.entries(socialMediaIcons)?.map(([key, Icon]) => { + const matchingSocialMedia = user.userSocialMedia.find(obj => obj.type === key); + if (!matchingSocialMedia) return null; + return ( + + + + ); + })} +
+
+
+ Bio + + {user.bio || Nothing here...} + +
+ {isSignedInUser && ( +
+ +
+ +
+ +
+ )} +
+ {(attendances || isSignedInUser) && ( +
+ Recently Attended Events + + {(isSignedInUser ? signedInAttendances : (attendances as PublicAttendance[])).map( + ({ event }) => ( + uuid === event.uuid)} + /> + ) + )} + +
+ )} +
+ ); +}; + +export default UserProfilePage; diff --git a/src/lib/api/EventAPI.ts b/src/lib/api/EventAPI.ts index a32531ca..5a289de7 100644 --- a/src/lib/api/EventAPI.ts +++ b/src/lib/api/EventAPI.ts @@ -73,7 +73,7 @@ export const getAllEvents = async (): Promise => { }; export const getAttendancesForUser = async (token: string): Promise => { - const requestUrl = `${config.api.baseUrl}${config.api.endpoints.attendance}`; + const requestUrl = `${config.api.baseUrl}${config.api.endpoints.attendance.attendance}`; const response = await axios.get(requestUrl, { headers: { @@ -88,7 +88,7 @@ export const attendEvent = async ( token: string, attendanceCode: string ): Promise => { - const requestUrl = `${config.api.baseUrl}${config.api.endpoints.attendance}`; + const requestUrl = `${config.api.baseUrl}${config.api.endpoints.attendance.attendance}`; const requestBody = { attendanceCode, asStaff: false } as AttendEventRequest; diff --git a/src/lib/api/UserAPI.ts b/src/lib/api/UserAPI.ts index 16df638f..48e00f6e 100644 --- a/src/lib/api/UserAPI.ts +++ b/src/lib/api/UserAPI.ts @@ -9,11 +9,13 @@ import { UserPatches, } from '@/lib/types/apiRequests'; import type { + GetAttendancesForUserResponse, GetCurrentUserResponse, GetUserResponse, InsertSocialMediaResponse, PatchUserResponse, PrivateProfile, + PublicAttendance, PublicProfile, PublicUserSocialMedia, UpdateProfilePictureResponse, @@ -165,3 +167,45 @@ export const deleteSocialMedia = async (token: string, uuid: UUID): Promise => { + const requestUrl = `${config.api.baseUrl}${config.api.endpoints.attendance.attendance}`; + + const response = await axios.get(requestUrl, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return response.data.attendances; +}; + +export const getAttendancesForUserByUUID = async ( + token: string, + uuid: UUID +): Promise => { + const requestUrl = `${config.api.baseUrl}${config.api.endpoints.attendance.forUserByUUID}/${uuid}`; + + const response = await axios.get(requestUrl, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return response.data.attendances; +}; + +export const getPublicUserProfileByUUID = async ( + token: string, + uuid: UUID +): Promise => { + const requestUrl = `${config.api.baseUrl}${config.api.endpoints.user.user}/${uuid}`; + + const response = await axios.get(requestUrl, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + return response.data.user; +}; diff --git a/src/lib/config.ts b/src/lib/config.ts index b0477cb1..f732bcac 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -34,7 +34,10 @@ const config = { future: '/event/future', picture: '/event/picture', }, - attendance: '/attendance', + attendance: { + attendance: '/attendance', + forUserByUUID: '/attendance/user', + }, leaderboard: '/leaderboard', store: { collection: '/merch/collection', diff --git a/src/lib/types/apiResponses.d.ts b/src/lib/types/apiResponses.d.ts index ba550fca..3834cbfc 100644 --- a/src/lib/types/apiResponses.d.ts +++ b/src/lib/types/apiResponses.d.ts @@ -332,7 +332,7 @@ export interface PublicProfile { major: string; bio: string | null; points: number; - userSocialMedia?: PublicUserSocialMedia[]; + userSocialMedia: PublicUserSocialMedia[]; isAttendancePublic: boolean; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index f58e90e6..08028b53 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -74,17 +74,23 @@ export const formatSearch = (text: string): string => { * Helper function to map each user to a numeric value deterministically * TODO: Use the user's UUID to hash to a number since it will never change * @param user - * @returns + * @returns A 32-bit integer + * @see https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript */ -const hashUser = (user: PublicProfile) => { - return user.points; +const hashUser = ({ uuid }: PublicProfile) => { + return uuid.split('').reduce((hash, char) => { + /* eslint-disable no-bitwise */ + const hash1 = (hash << 5) - hash + char.charCodeAt(0); + return hash1 | 0; // Convert to 32bit integer + /* eslint-enable no-bitwise */ + }, 0); }; export const getProfilePicture = (user: PublicProfile): URL => { if (user.profilePicture) return user.profilePicture; const NUM_IMAGES = defaultProfilePictures.length; - const index = hashUser(user) % NUM_IMAGES; + const index = ((hashUser(user) % NUM_IMAGES) + NUM_IMAGES) % NUM_IMAGES; const path = defaultProfilePictures[index]?.src ?? ''; return path; @@ -99,12 +105,32 @@ export const getLevel = (points: number): number => { return Math.floor(points / 100) + 1; }; -// TODO: Define all ranks and logic for this -export const getUserRank = (user: PublicProfile): string => { - const ranks = ['Polynomial Pita', 'Factorial Flatbread']; - const index = user.points % 2; - - return ranks[index] ?? ''; +/** + * Get a user's level and rank based on how many points they have + * @param points + * @returns [numeric level, rank name] + */ +export const getUserRank = (points: number): [number, string] => { + const ranks = [ + 'Factorial Flatbread', + 'Exponential Eclair', + 'Polynomial Pita', + 'Cubic Croissant', + 'Quadratic Qornbread', + 'Linear Loaf', + 'nlog Naan', + 'Constant Cornbread', + 'Binary Baguette', + 'Blessed Boba', + 'Super Snu', + 'Soon(TM)', + 'Later(TM)', + 'Sometime(TM)', + 'We Ran Out Of Ranks', + ] as const; + const level = Math.floor(points / 100) + 1; + const index = Math.min(ranks.length - 1, level - 1); + return [level, ranks[index] as string]; }; /** diff --git a/src/pages/leaderboard.tsx b/src/pages/leaderboard.tsx index 592b6bb1..d52a9621 100644 --- a/src/pages/leaderboard.tsx +++ b/src/pages/leaderboard.tsx @@ -123,7 +123,7 @@ const LeaderboardPage = ({ sort, leaderboard, user: { uuid } }: LeaderboardProps { - return ( - <> -

Portal Profile Page

- - Manage Account - - - ); + return

Portal Profile Page

; }; export default UserProfilePage; -const getServerSidePropsFunc: GetServerSideProps = async () => ({ - props: {}, -}); +const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => { + const user = JSON.parse(CookieService.getServerCookie(CookieType.USER, { req, res })); + + return { + redirect: { + destination: `/u/${user.handle}`, + permanent: true, + }, + }; +}; export const getServerSideProps = withAccessType( getServerSidePropsFunc, diff --git a/src/pages/u/[handle].tsx b/src/pages/u/[handle].tsx index 98b20c60..65324a87 100644 --- a/src/pages/u/[handle].tsx +++ b/src/pages/u/[handle].tsx @@ -1,17 +1,169 @@ +import { Carousel, Typography } from '@/components/common'; +import EventCard from '@/components/events/EventCard'; import { config } from '@/lib'; import { UserAPI } from '@/lib/api'; import withAccessType from '@/lib/hoc/withAccessType'; import { CookieService, PermissionService } from '@/lib/services'; -import type { PublicProfile } from '@/lib/types/apiResponses'; +import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; import { CookieType } from '@/lib/types/enums'; +import { getProfilePicture, getUserRank } from '@/lib/utils'; +import DevpostIcon from '@/public/assets/icons/devpost-icon.svg'; +import EditIcon from '@/public/assets/icons/edit.svg'; +import FacebookIcon from '@/public/assets/icons/facebook-icon.svg'; +import GithubIcon from '@/public/assets/icons/github-icon.svg'; +import InstagramIcon from '@/public/assets/icons/instagram.svg'; +import LeaderboardIcon from '@/public/assets/icons/leaderboard-icon.svg'; +import LinkedinIcon from '@/public/assets/icons/linkedin-icon.svg'; +import MajorIcon from '@/public/assets/icons/major-icon.svg'; +import ProfileIcon from '@/public/assets/icons/profile-icon.svg'; +import styles from '@/styles/pages/u/index.module.scss'; +import Image from 'next/image'; +import Link from 'next/link'; import type { GetServerSideProps } from 'next/types'; +import { useEffect, useState } from 'react'; +import { AiOutlineLink } from 'react-icons/ai'; +import { IoMail } from 'react-icons/io5'; interface UserProfilePageProps { user: PublicProfile; + isSignedInUser: boolean; + signedInAttendances: PublicAttendance[]; + attendances?: PublicAttendance[]; } -const UserProfilePage = ({ user }: UserProfilePageProps) => { - return
{JSON.stringify(user, null, 2)}
; +const socialMediaIcons = { + LINKEDIN: LinkedinIcon, + GITHUB: GithubIcon, + DEVPOST: DevpostIcon, + PORTFOLIO: AiOutlineLink, + FACEBOOK: FacebookIcon, + INSTAGRAM: InstagramIcon, + EMAIL: IoMail, +}; + +const UserProfilePage = ({ + user, + attendances, + signedInAttendances, + isSignedInUser, +}: UserProfilePageProps) => { + // animate the progress bar + const [progress, setProgress] = useState(0); + useEffect(() => setProgress(user.points % 100), [user.points]); + + return ( +
+
+
+
+
+ Profile Picture +
+
+ {`${user.firstName} ${user.lastName}`} + @{user.handle} +
+
+
{getUserRank(user.points)[1]}
+
+   + {user.points.toLocaleString()} Leaderboard Points +
+
+ {isSignedInUser && ( +
+ +
+ +
+ +
+ )} +
+
+
+ + {isSignedInUser ? 'My' : `${user.firstName}'s`} Progress + +
+ Level {getUserRank(user.points)[0]} + {user.points % 100}/100 +
+
+
+
+ + {isSignedInUser ? 'You need' : `${user.firstName} needs`} {100 - (user.points % 100)} more + points to level up to + +  {getUserRank(user.points + 100)[1]} + + +
+ +
+
+ About me +
+ + Class of {user.graduationYear} + + {user.major} +
+
+ {user.userSocialMedia && + Object.entries(socialMediaIcons)?.map(([key, Icon]) => { + const matchingSocialMedia = user.userSocialMedia.find(obj => obj.type === key); + if (!matchingSocialMedia) return null; + return ( + + + + ); + })} +
+
+
+ Bio + + {user.bio || Nothing here...} + +
+ {isSignedInUser && ( +
+ +
+ +
+ +
+ )} +
+ {(attendances || isSignedInUser) && ( +
+ Recently Attended Events + + {(isSignedInUser ? signedInAttendances : (attendances as PublicAttendance[])).map( + ({ event }) => ( + uuid === event.uuid)} + /> + ) + )} + +
+ )} +
+ ); }; export default UserProfilePage; @@ -21,10 +173,25 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res }); try { - const user = await UserAPI.getUserByHandle(token, handle); + const [user, signedInUser, signedInAttendances] = await Promise.all([ + UserAPI.getUserByHandle(token, handle), + UserAPI.getCurrentUser(token), + UserAPI.getAttendancesForCurrentUser(token), + ]); + const isSignedInUser = user.uuid === signedInUser.uuid; + + if (!user.isAttendancePublic || isSignedInUser) { + return { props: { user, isSignedInUser, signedInAttendances } }; + } + + const attendances = await UserAPI.getAttendancesForUserByUUID(token, user.uuid); + return { props: { user, + isSignedInUser, + signedInAttendances, + attendances, }, }; } catch (err: any) { diff --git a/src/styles/pages/u/index.module.scss b/src/styles/pages/u/index.module.scss new file mode 100644 index 00000000..1e629620 --- /dev/null +++ b/src/styles/pages/u/index.module.scss @@ -0,0 +1,201 @@ +@use 'src/styles/vars.scss' as vars; + +.profilePage { + display: flex; + flex-direction: column; + gap: 2rem; + margin: auto; + max-width: vars.$breakpoint-md; + width: 100%; + + .cardWrapper { + padding-top: 3rem; + position: relative; + + .banner { + background-color: vars.$pink-2; + border-radius: 0.5rem; + height: 90%; + position: absolute; + top: 0; + width: 100%; + } + + .profileCard { + background-color: var(--theme-background); + border-radius: 0.25rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25); + display: flex; + gap: 1rem; + margin: 0 2rem; + padding: 1rem 2rem; + position: relative; + + @media (max-width: vars.$breakpoint-md) { + align-items: center; + flex-direction: column; + } + + .profilePic { + border-radius: 4rem; + flex-shrink: 0; + height: 7rem; + overflow: hidden; + position: relative; + width: 7rem; + } + + .cardName { + display: flex; + flex-direction: column; + justify-content: center; + + @media (max-width: vars.$breakpoint-md) { + align-items: center; + flex-direction: column; + } + } + + .cardRank { + align-items: flex-end; + display: flex; + flex-direction: column; + flex-grow: 1; + justify-content: flex-end; + + @media (max-width: vars.$breakpoint-md) { + align-items: center; + flex-direction: column; + } + + .rank { + color: var(--theme-text-on-background-1); + display: flex; + font-size: 1.25rem; + font-weight: 500; + text-align: center; + } + + .points { + color: var(--theme-text-on-background-1); + font-size: 1.125rem; + font-weight: 400; + line-height: 150%; + text-align: center; + + svg { + height: 1.125rem; + width: 1.125rem; + } + } + } + } + } + + .section { + border: 2px solid #808184; + border-radius: 0.75rem; + padding: 1rem 1.5rem; + } + + .progressSection { + display: 'flex'; + flex-direction: column; + gap: 1rem; + + .progressInfo { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + } + + .progressBar { + background-color: var(--theme-text-on-background-3); + border-radius: 0.5rem; + height: 1.25rem; + width: 100%; + + .inner { + background-color: var(--theme-primary-2); + border-radius: 0.5rem; + height: 100%; + transition: width 1s ease; + } + } + + p { + @media (max-width: vars.$breakpoint-md) { + text-align: center; + } + } + } + + .aboutSection { + display: grid; + grid-template-columns: 20rem 1fr; + position: relative; + + @media (max-width: vars.$breakpoint-md) { + display: flex; + flex-direction: column; + gap: 1.875rem; + } + + .aboutMeSection { + display: grid; + gap: 0.56rem; + grid-template-columns: auto 1fr; + margin: 0.81rem 0; + + .icon { + height: 1.313rem; + width: 1.313rem; + } + + } + + svg { + height: 2rem; + width: 2rem; + } + + .bioSection { + display: flex; + flex-direction: column; + gap: 0.5rem; + } + } + + .card { + flex: none; + width: 20rem; + } + + .editWrapper { + position: absolute; + right: 1rem; + top: 1rem; + + div { + align-items: center; + background: var(--theme-background); + border: 1px solid var(--theme-accent-1-transparent); + border-radius: 0.625rem; + box-shadow: 0 0.0625rem 0.25rem 0 var(--theme-shadow); + display: flex; + height: 2.375rem; + justify-content: center; + transition: all 0.3s; + width: 2.75rem; + + &:hover { + transform: scale(1.1); + } + + svg { + height: 1.625rem; + width: 1.625rem; + } + } + } +} \ No newline at end of file diff --git a/src/styles/pages/u/index.module.scss.d.ts b/src/styles/pages/u/index.module.scss.d.ts new file mode 100644 index 00000000..f328d500 --- /dev/null +++ b/src/styles/pages/u/index.module.scss.d.ts @@ -0,0 +1,28 @@ +export type Styles = { + aboutMeSection: string; + aboutSection: string; + banner: string; + bioSection: string; + card: string; + cardName: string; + cardRank: string; + cardWrapper: string; + editWrapper: string; + icon: string; + inner: string; + points: string; + profileCard: string; + profilePage: string; + profilePic: string; + progressBar: string; + progressInfo: string; + progressSection: string; + rank: string; + section: string; +}; + +export type ClassNames = keyof Styles; + +declare const styles: Styles; + +export default styles; From 1be010e83e2e11c0dc0245582dc7211ddd725f04 Mon Sep 17 00:00:00 2001 From: raymosun Date: Sun, 7 Jan 2024 22:07:26 -0800 Subject: [PATCH 02/22] Implement handle-not-found functionality --- .../profile/HandleNotFoundPage/index.tsx | 18 ++ .../profile/UserProfilePage/index.tsx | 6 +- src/components/profile/index.ts | 3 + src/pages/u/[handle].tsx | 182 +++--------------- 4 files changed, 46 insertions(+), 163 deletions(-) create mode 100644 src/components/profile/HandleNotFoundPage/index.tsx diff --git a/src/components/profile/HandleNotFoundPage/index.tsx b/src/components/profile/HandleNotFoundPage/index.tsx new file mode 100644 index 00000000..da53066e --- /dev/null +++ b/src/components/profile/HandleNotFoundPage/index.tsx @@ -0,0 +1,18 @@ +import { SignInButton } from '@/components/auth'; +import { Typography, VerticalForm } from '@/components/common'; +import Cat404 from '@/public/assets/graphics/cat404.png'; +import Image from 'next/image'; + +export interface HandleNotFoundPageProps { + handle: string; +} + +export const HandleNotFoundPage = ({ handle }: HandleNotFoundPageProps) => ( + + + No user with handle ‘{handle}’ was found. + + Sad Cat + + +); diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index ecccaf3b..268abbe1 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -19,7 +19,7 @@ import { useEffect, useState } from 'react'; import { AiOutlineLink } from 'react-icons/ai'; import { IoMail } from 'react-icons/io5'; -interface UserProfilePageProps { +export interface UserProfilePageProps { user: PublicProfile; isSignedInUser: boolean; signedInAttendances: PublicAttendance[]; @@ -36,7 +36,7 @@ const socialMediaIcons = { EMAIL: IoMail, }; -const UserProfilePage = ({ +export const UserProfilePage = ({ user, attendances, signedInAttendances, @@ -160,5 +160,3 @@ const UserProfilePage = ({
); }; - -export default UserProfilePage; diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index 2304b886..46481b1a 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -1,4 +1,7 @@ export { EditBlock, EditField, SingleField } from './EditField'; +export * from './HandleNotFoundPage'; export { default as Preview } from './Preview'; export { default as SocialMediaIcon } from './SocialMediaIcon'; export { default as Switch } from './Switch'; +export * from './UserProfilePage'; + diff --git a/src/pages/u/[handle].tsx b/src/pages/u/[handle].tsx index 65324a87..89e315fd 100644 --- a/src/pages/u/[handle].tsx +++ b/src/pages/u/[handle].tsx @@ -1,172 +1,31 @@ -import { Carousel, Typography } from '@/components/common'; -import EventCard from '@/components/events/EventCard'; +import { + HandleNotFoundPage, + HandleNotFoundPageProps, + UserProfilePage, + UserProfilePageProps, +} from '@/components/profile'; import { config } from '@/lib'; import { UserAPI } from '@/lib/api'; import withAccessType from '@/lib/hoc/withAccessType'; import { CookieService, PermissionService } from '@/lib/services'; -import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; import { CookieType } from '@/lib/types/enums'; -import { getProfilePicture, getUserRank } from '@/lib/utils'; -import DevpostIcon from '@/public/assets/icons/devpost-icon.svg'; -import EditIcon from '@/public/assets/icons/edit.svg'; -import FacebookIcon from '@/public/assets/icons/facebook-icon.svg'; -import GithubIcon from '@/public/assets/icons/github-icon.svg'; -import InstagramIcon from '@/public/assets/icons/instagram.svg'; -import LeaderboardIcon from '@/public/assets/icons/leaderboard-icon.svg'; -import LinkedinIcon from '@/public/assets/icons/linkedin-icon.svg'; -import MajorIcon from '@/public/assets/icons/major-icon.svg'; -import ProfileIcon from '@/public/assets/icons/profile-icon.svg'; -import styles from '@/styles/pages/u/index.module.scss'; -import Image from 'next/image'; -import Link from 'next/link'; import type { GetServerSideProps } from 'next/types'; -import { useEffect, useState } from 'react'; -import { AiOutlineLink } from 'react-icons/ai'; -import { IoMail } from 'react-icons/io5'; -interface UserProfilePageProps { - user: PublicProfile; - isSignedInUser: boolean; - signedInAttendances: PublicAttendance[]; - attendances?: PublicAttendance[]; -} +type UserHandlePageProps = HandleNotFoundPageProps | UserProfilePageProps; -const socialMediaIcons = { - LINKEDIN: LinkedinIcon, - GITHUB: GithubIcon, - DEVPOST: DevpostIcon, - PORTFOLIO: AiOutlineLink, - FACEBOOK: FacebookIcon, - INSTAGRAM: InstagramIcon, - EMAIL: IoMail, -}; - -const UserProfilePage = ({ - user, - attendances, - signedInAttendances, - isSignedInUser, -}: UserProfilePageProps) => { - // animate the progress bar - const [progress, setProgress] = useState(0); - useEffect(() => setProgress(user.points % 100), [user.points]); +const isHandleNotFound = (props: UserHandlePageProps): props is HandleNotFoundPageProps => + 'handle' in props; - return ( -
-
-
-
-
- Profile Picture -
-
- {`${user.firstName} ${user.lastName}`} - @{user.handle} -
-
-
{getUserRank(user.points)[1]}
-
-   - {user.points.toLocaleString()} Leaderboard Points -
-
- {isSignedInUser && ( -
- -
- -
- -
- )} -
-
-
- - {isSignedInUser ? 'My' : `${user.firstName}'s`} Progress - -
- Level {getUserRank(user.points)[0]} - {user.points % 100}/100 -
-
-
-
- - {isSignedInUser ? 'You need' : `${user.firstName} needs`} {100 - (user.points % 100)} more - points to level up to - -  {getUserRank(user.points + 100)[1]} - - -
+const UserHandlePage = (props: UserHandlePageProps) => { + if (isHandleNotFound(props)) { + const { handle } = props; + return ; + } -
-
- About me -
- - Class of {user.graduationYear} - - {user.major} -
-
- {user.userSocialMedia && - Object.entries(socialMediaIcons)?.map(([key, Icon]) => { - const matchingSocialMedia = user.userSocialMedia.find(obj => obj.type === key); - if (!matchingSocialMedia) return null; - return ( - - - - ); - })} -
-
-
- Bio - - {user.bio || Nothing here...} - -
- {isSignedInUser && ( -
- -
- -
- -
- )} -
- {(attendances || isSignedInUser) && ( -
- Recently Attended Events - - {(isSignedInUser ? signedInAttendances : (attendances as PublicAttendance[])).map( - ({ event }) => ( - uuid === event.uuid)} - /> - ) - )} - -
- )} -
- ); + return ; }; -export default UserProfilePage; +export default UserHandlePage; const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) => { const handle = params?.handle as string; @@ -174,18 +33,23 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) try { const [user, signedInUser, signedInAttendances] = await Promise.all([ - UserAPI.getUserByHandle(token, handle), + UserAPI.getUserByHandle(token, handle).catch(() => null), UserAPI.getCurrentUser(token), UserAPI.getAttendancesForCurrentUser(token), ]); + + // render HandleNotFoundPage + if (user === null) return { props: { handle } }; + const isSignedInUser = user.uuid === signedInUser.uuid; + // UserProfilePage without separate user attendance if (!user.isAttendancePublic || isSignedInUser) { return { props: { user, isSignedInUser, signedInAttendances } }; } + // UserProfilePage with separate user and signed-in attendances const attendances = await UserAPI.getAttendancesForUserByUUID(token, user.uuid); - return { props: { user, From 4d9cfd350c41287754f61ce6a72851438035957f Mon Sep 17 00:00:00 2001 From: raymosun Date: Sun, 7 Jan 2024 22:25:57 -0800 Subject: [PATCH 03/22] Fix icons --- public/assets/icons/ig-icon.svg | 1 - src/components/home/Hero/style.module.scss | 5 +++++ src/pages/about.tsx | 8 ++++---- src/styles/pages/about.module.scss | 4 +++- src/styles/pages/about.module.scss.d.ts | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) delete mode 100644 public/assets/icons/ig-icon.svg diff --git a/public/assets/icons/ig-icon.svg b/public/assets/icons/ig-icon.svg deleted file mode 100644 index 6fcb443d..00000000 --- a/public/assets/icons/ig-icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/home/Hero/style.module.scss b/src/components/home/Hero/style.module.scss index b150c8de..6aa03e87 100644 --- a/src/components/home/Hero/style.module.scss +++ b/src/components/home/Hero/style.module.scss @@ -122,6 +122,11 @@ &:hover { box-shadow: 0 5px 10px var(--theme-shadow); } + + svg { + height: 1.5rem; + width: 1.5rem; + } } } } diff --git a/src/pages/about.tsx b/src/pages/about.tsx index 8bb46390..d96f6f44 100644 --- a/src/pages/about.tsx +++ b/src/pages/about.tsx @@ -34,19 +34,19 @@ const AboutPage = () => { diff --git a/src/styles/pages/about.module.scss b/src/styles/pages/about.module.scss index 4f6d4e78..67757f52 100644 --- a/src/styles/pages/about.module.scss +++ b/src/styles/pages/about.module.scss @@ -45,8 +45,10 @@ gap: 0.5rem; margin-bottom: 0.7rem; - .theme { + .icon { fill: currentColor; + height: 1.5rem; + width: 1.5rem; } } } diff --git a/src/styles/pages/about.module.scss.d.ts b/src/styles/pages/about.module.scss.d.ts index b50acecd..e0297ef6 100644 --- a/src/styles/pages/about.module.scss.d.ts +++ b/src/styles/pages/about.module.scss.d.ts @@ -4,9 +4,9 @@ export type Styles = { description: string; discordPreview: string; header: string; + icon: string; logo: string; socials: string; - theme: string; }; export type ClassNames = keyof Styles; From fcf72e05d0c884085965b4f55844add9e6b600ee Mon Sep 17 00:00:00 2001 From: raymosun Date: Sun, 7 Jan 2024 23:05:11 -0800 Subject: [PATCH 04/22] Clean up code --- src/components/home/Hero/style.module.scss | 4 ++-- .../{HandleNotFoundPage => HandleNotFound}/index.tsx | 4 ++-- .../profile/UserProfilePage}/index.module.scss | 0 .../profile/UserProfilePage}/index.module.scss.d.ts | 0 src/components/profile/UserProfilePage/index.tsx | 10 +++++----- src/components/profile/index.ts | 3 +-- src/lib/utils.ts | 12 +++++------- src/pages/leaderboard.tsx | 4 ++-- src/pages/u/[handle].tsx | 12 ++++++------ src/styles/pages/about.module.scss | 4 ++-- 10 files changed, 25 insertions(+), 28 deletions(-) rename src/components/profile/{HandleNotFoundPage => HandleNotFound}/index.tsx (83%) rename src/{styles/pages/u => components/profile/UserProfilePage}/index.module.scss (100%) rename src/{styles/pages/u => components/profile/UserProfilePage}/index.module.scss.d.ts (100%) diff --git a/src/components/home/Hero/style.module.scss b/src/components/home/Hero/style.module.scss index 6aa03e87..77973d2c 100644 --- a/src/components/home/Hero/style.module.scss +++ b/src/components/home/Hero/style.module.scss @@ -124,8 +124,8 @@ } svg { - height: 1.5rem; - width: 1.5rem; + height: 24px; + width: 24px; } } } diff --git a/src/components/profile/HandleNotFoundPage/index.tsx b/src/components/profile/HandleNotFound/index.tsx similarity index 83% rename from src/components/profile/HandleNotFoundPage/index.tsx rename to src/components/profile/HandleNotFound/index.tsx index da53066e..74c3edd3 100644 --- a/src/components/profile/HandleNotFoundPage/index.tsx +++ b/src/components/profile/HandleNotFound/index.tsx @@ -3,11 +3,11 @@ import { Typography, VerticalForm } from '@/components/common'; import Cat404 from '@/public/assets/graphics/cat404.png'; import Image from 'next/image'; -export interface HandleNotFoundPageProps { +export interface HandleNotFoundProps { handle: string; } -export const HandleNotFoundPage = ({ handle }: HandleNotFoundPageProps) => ( +export const HandleNotFound = ({ handle }: HandleNotFoundProps) => ( No user with handle ‘{handle}’ was found. diff --git a/src/styles/pages/u/index.module.scss b/src/components/profile/UserProfilePage/index.module.scss similarity index 100% rename from src/styles/pages/u/index.module.scss rename to src/components/profile/UserProfilePage/index.module.scss diff --git a/src/styles/pages/u/index.module.scss.d.ts b/src/components/profile/UserProfilePage/index.module.scss.d.ts similarity index 100% rename from src/styles/pages/u/index.module.scss.d.ts rename to src/components/profile/UserProfilePage/index.module.scss.d.ts diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index 268abbe1..9ae68707 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -2,7 +2,7 @@ import { Carousel, Typography } from '@/components/common'; import EventCard from '@/components/events/EventCard'; import { config } from '@/lib'; import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; -import { getProfilePicture, getUserRank } from '@/lib/utils'; +import { getLevel, getProfilePicture, getUserRank } from '@/lib/utils'; import DevpostIcon from '@/public/assets/icons/devpost-icon.svg'; import EditIcon from '@/public/assets/icons/edit.svg'; import FacebookIcon from '@/public/assets/icons/facebook-icon.svg'; @@ -12,12 +12,12 @@ import LeaderboardIcon from '@/public/assets/icons/leaderboard-icon.svg'; import LinkedinIcon from '@/public/assets/icons/linkedin-icon.svg'; import MajorIcon from '@/public/assets/icons/major-icon.svg'; import ProfileIcon from '@/public/assets/icons/profile-icon.svg'; -import styles from '@/styles/pages/u/index.module.scss'; import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { AiOutlineLink } from 'react-icons/ai'; import { IoMail } from 'react-icons/io5'; +import styles from './index.module.scss'; export interface UserProfilePageProps { user: PublicProfile; @@ -65,7 +65,7 @@ export const UserProfilePage = ({ @{user.handle}
-
{getUserRank(user.points)[1]}
+
{getUserRank(user.points)}
  {user.points.toLocaleString()} Leaderboard Points @@ -87,7 +87,7 @@ export const UserProfilePage = ({ {isSignedInUser ? 'My' : `${user.firstName}'s`} Progress
- Level {getUserRank(user.points)[0]} + Level {getLevel(user.points)} {user.points % 100}/100
@@ -97,7 +97,7 @@ export const UserProfilePage = ({ {isSignedInUser ? 'You need' : `${user.firstName} needs`} {100 - (user.points % 100)} more points to level up to -  {getUserRank(user.points + 100)[1]} +  {getUserRank(user.points + 100)}
diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index 46481b1a..5dde5283 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -1,7 +1,6 @@ export { EditBlock, EditField, SingleField } from './EditField'; -export * from './HandleNotFoundPage'; +export * from './HandleNotFound'; export { default as Preview } from './Preview'; export { default as SocialMediaIcon } from './SocialMediaIcon'; export { default as Switch } from './Switch'; export * from './UserProfilePage'; - diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 08028b53..e289b272 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -72,7 +72,6 @@ export const formatSearch = (text: string): string => { /** * Helper function to map each user to a numeric value deterministically - * TODO: Use the user's UUID to hash to a number since it will never change * @param user * @returns A 32-bit integer * @see https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript @@ -106,11 +105,11 @@ export const getLevel = (points: number): number => { }; /** - * Get a user's level and rank based on how many points they have + * Get a user's rank based on how many points they have * @param points - * @returns [numeric level, rank name] + * @returns rank name */ -export const getUserRank = (points: number): [number, string] => { +export const getUserRank = (points: number): string => { const ranks = [ 'Factorial Flatbread', 'Exponential Eclair', @@ -128,9 +127,8 @@ export const getUserRank = (points: number): [number, string] => { 'Sometime(TM)', 'We Ran Out Of Ranks', ] as const; - const level = Math.floor(points / 100) + 1; - const index = Math.min(ranks.length - 1, level - 1); - return [level, ranks[index] as string]; + const index = Math.min(ranks.length, getLevel(points)) - 1; + return ranks[index] as string; }; /** diff --git a/src/pages/leaderboard.tsx b/src/pages/leaderboard.tsx index d52a9621..4b6c4e68 100644 --- a/src/pages/leaderboard.tsx +++ b/src/pages/leaderboard.tsx @@ -123,7 +123,7 @@ const LeaderboardPage = ({ sort, leaderboard, user: { uuid } }: LeaderboardProps +const isHandleNotFound = (props: UserHandlePageProps): props is HandleNotFoundProps => 'handle' in props; const UserHandlePage = (props: UserHandlePageProps) => { if (isHandleNotFound(props)) { const { handle } = props; - return ; + return ; } return ; @@ -38,7 +38,7 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) UserAPI.getAttendancesForCurrentUser(token), ]); - // render HandleNotFoundPage + // render HandleNotFoundPage when user with handle is not retrieved if (user === null) return { props: { handle } }; const isSignedInUser = user.uuid === signedInUser.uuid; diff --git a/src/styles/pages/about.module.scss b/src/styles/pages/about.module.scss index 67757f52..6cc06b17 100644 --- a/src/styles/pages/about.module.scss +++ b/src/styles/pages/about.module.scss @@ -47,8 +47,8 @@ .icon { fill: currentColor; - height: 1.5rem; - width: 1.5rem; + height: 24px; + width: 24px; } } } From 4d23e8a4e349d95f16eb69cad5acd623327dd1b5 Mon Sep 17 00:00:00 2001 From: raymosun Date: Sun, 7 Jan 2024 23:10:41 -0800 Subject: [PATCH 05/22] Update file names --- src/components/profile/UserProfilePage/index.tsx | 2 +- .../UserProfilePage/{index.module.scss => style.module.scss} | 0 .../{index.module.scss.d.ts => style.module.scss.d.ts} | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/components/profile/UserProfilePage/{index.module.scss => style.module.scss} (100%) rename src/components/profile/UserProfilePage/{index.module.scss.d.ts => style.module.scss.d.ts} (100%) diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index 9ae68707..9cfe8499 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -17,7 +17,7 @@ import Link from 'next/link'; import { useEffect, useState } from 'react'; import { AiOutlineLink } from 'react-icons/ai'; import { IoMail } from 'react-icons/io5'; -import styles from './index.module.scss'; +import styles from './style.module.scss'; export interface UserProfilePageProps { user: PublicProfile; diff --git a/src/components/profile/UserProfilePage/index.module.scss b/src/components/profile/UserProfilePage/style.module.scss similarity index 100% rename from src/components/profile/UserProfilePage/index.module.scss rename to src/components/profile/UserProfilePage/style.module.scss diff --git a/src/components/profile/UserProfilePage/index.module.scss.d.ts b/src/components/profile/UserProfilePage/style.module.scss.d.ts similarity index 100% rename from src/components/profile/UserProfilePage/index.module.scss.d.ts rename to src/components/profile/UserProfilePage/style.module.scss.d.ts From a5fc284a3ef1124f6adfb3b6f119ea875756748b Mon Sep 17 00:00:00 2001 From: Raymond Sun <30709485+raymosun@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:08:39 -0800 Subject: [PATCH 06/22] Update src/components/profile/UserProfilePage/index.tsx Co-authored-by: Faris Ashai --- src/components/profile/UserProfilePage/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index 9cfe8499..66378517 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -1,5 +1,5 @@ import { Carousel, Typography } from '@/components/common'; -import EventCard from '@/components/events/EventCard'; +import { EventCard } from '@/components/events'; import { config } from '@/lib'; import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; import { getLevel, getProfilePicture, getUserRank } from '@/lib/utils'; From aa3fd06be32bc875f5d7baf928ddf19dfb90ec46 Mon Sep 17 00:00:00 2001 From: raymosun Date: Mon, 8 Jan 2024 17:00:50 -0800 Subject: [PATCH 07/22] Implement the GifSafeImage component --- src/components/common/GifSafeImage/index.tsx | 13 +++++++++++++ src/components/common/index.ts | 1 + src/components/leaderboard/LeaderboardRow/index.tsx | 6 ++---- src/components/leaderboard/TopThreeCard/index.tsx | 7 +++---- src/components/profile/Preview/index.tsx | 7 +++---- src/components/profile/UserProfilePage/index.tsx | 4 ++-- src/lib/utils.ts | 11 +++++++++-- src/pages/profile/edit.tsx | 7 +++---- 8 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/components/common/GifSafeImage/index.tsx diff --git a/src/components/common/GifSafeImage/index.tsx b/src/components/common/GifSafeImage/index.tsx new file mode 100644 index 00000000..9d95c238 --- /dev/null +++ b/src/components/common/GifSafeImage/index.tsx @@ -0,0 +1,13 @@ +import { isSrcAGif } from '@/lib/utils'; +import Image, { ImageProps } from 'next/image'; + +/** + * A next/image Image that is automatically unoptimized when `src` is a gif + * @param props - props for Next Image component + * @returns image component + */ +const GifSafeImage = ({ src, alt, ...restProps }: ImageProps) => { + return {alt}; +}; + +export default GifSafeImage; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 4eb274f1..40711238 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -3,6 +3,7 @@ export { default as Carousel } from './Carousel'; export { default as CommunityLogo } from './CommunityLogo'; export { default as Cropper } from './Cropper'; export { default as Dropdown } from './Dropdown'; +export { default as GifSafeImage } from './GifSafeImage'; export { default as LinkButton } from './LinkButton'; export { default as Modal } from './Modal'; export { default as PaginationControls } from './PaginationControls'; diff --git a/src/components/leaderboard/LeaderboardRow/index.tsx b/src/components/leaderboard/LeaderboardRow/index.tsx index 3209df37..db101939 100644 --- a/src/components/leaderboard/LeaderboardRow/index.tsx +++ b/src/components/leaderboard/LeaderboardRow/index.tsx @@ -1,5 +1,4 @@ -import { isSrcAGif } from '@/lib/utils'; -import Image from 'next/image'; +import GifSafeImage from '@/components/common/GifSafeImage'; import Link from 'next/link'; import { useEffect, useRef } from 'react'; import styles from './style.module.scss'; @@ -52,14 +51,13 @@ const LeaderboardRow = ({ return ( {position} - {`Profile
diff --git a/src/components/leaderboard/TopThreeCard/index.tsx b/src/components/leaderboard/TopThreeCard/index.tsx index 763e4d59..44ccb159 100644 --- a/src/components/leaderboard/TopThreeCard/index.tsx +++ b/src/components/leaderboard/TopThreeCard/index.tsx @@ -1,5 +1,5 @@ -import { isSrcAGif, trim } from '@/lib/utils'; -import Image from 'next/image'; +import GifSafeImage from '@/components/common/GifSafeImage'; +import { trim } from '@/lib/utils'; import Link from 'next/link'; import styles from './style.module.scss'; @@ -18,13 +18,12 @@ const TopThreeCard = ({ position, rank, name, url, points, image }: UserCardProp
{position}
- User Profile Pic {trim(name, 25)} {rank} diff --git a/src/components/profile/Preview/index.tsx b/src/components/profile/Preview/index.tsx index a4a37b8c..1fa1cb80 100644 --- a/src/components/profile/Preview/index.tsx +++ b/src/components/profile/Preview/index.tsx @@ -1,9 +1,9 @@ import { Typography } from '@/components/common'; +import GifSafeImage from '@/components/common/GifSafeImage'; import SocialMediaIcon from '@/components/profile/SocialMediaIcon'; import { PublicProfile } from '@/lib/types/apiResponses'; import { SocialMediaType } from '@/lib/types/enums'; -import { getLevel, getProfilePicture, isSrcAGif } from '@/lib/utils'; -import Image from 'next/image'; +import { getLevel, getProfilePicture } from '@/lib/utils'; import styles from './style.module.scss'; const fixUrl = (url: string) => (url.includes('://') ? url : `http://${url}`); @@ -31,13 +31,12 @@ interface PreviewProps { const Preview = ({ user, pfpCacheBust }: PreviewProps) => { return (
- Profile picture {user.firstName} {user.lastName} diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index 9cfe8499..cf005118 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -1,4 +1,5 @@ import { Carousel, Typography } from '@/components/common'; +import GifSafeImage from '@/components/common/GifSafeImage'; import EventCard from '@/components/events/EventCard'; import { config } from '@/lib'; import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; @@ -12,7 +13,6 @@ import LeaderboardIcon from '@/public/assets/icons/leaderboard-icon.svg'; import LinkedinIcon from '@/public/assets/icons/linkedin-icon.svg'; import MajorIcon from '@/public/assets/icons/major-icon.svg'; import ProfileIcon from '@/public/assets/icons/profile-icon.svg'; -import Image from 'next/image'; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { AiOutlineLink } from 'react-icons/ai'; @@ -52,7 +52,7 @@ export const UserProfilePage = ({
- Profile Picture { }; /** - * Checks whether an image source is a gif + * Checks whether a next/image Image source is a gif * @param src - source of the image * @returns whether or not the source is a gif */ -export const isSrcAGif = (src: string | null): boolean => src !== null && /\.gif($|&)/.test(src); +export const isSrcAGif = (src: string | StaticImport): boolean => { + const srcString = + (typeof src === 'string' && src) || + (src as StaticRequire).default?.src || + (src as StaticImageData).src; + return /\.gif($|&)/.test(srcString); +}; /** * A React hook for calling `URL.createObjectURL` on the given file. Avoids diff --git a/src/pages/profile/edit.tsx b/src/pages/profile/edit.tsx index a35932ae..2a47424b 100644 --- a/src/pages/profile/edit.tsx +++ b/src/pages/profile/edit.tsx @@ -1,4 +1,5 @@ import { Cropper } from '@/components/common'; +import GifSafeImage from '@/components/common/GifSafeImage'; import { EditBlock, EditField, @@ -15,13 +16,12 @@ import withAccessType from '@/lib/hoc/withAccessType'; import { CookieService, PermissionService } from '@/lib/services'; import { PrivateProfile } from '@/lib/types/apiResponses'; import { CookieType, SocialMediaType } from '@/lib/types/enums'; -import { capitalize, getMessagesFromError, getProfilePicture, isSrcAGif } from '@/lib/utils'; +import { capitalize, getMessagesFromError, getProfilePicture } from '@/lib/utils'; import DownloadIcon from '@/public/assets/icons/download-icon.svg'; import DropdownIcon from '@/public/assets/icons/dropdown-arrow-1.svg'; import styles from '@/styles/pages/profile/edit.module.scss'; import { AxiosError } from 'axios'; import type { GetServerSideProps } from 'next'; -import Image from 'next/image'; import Link from 'next/link'; import { FormEvent, useEffect, useId, useMemo, useState } from 'react'; @@ -278,7 +278,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => { onDragLeave={() => setPfpDrop(false)} >
From 50b32a4e0ad921a94c9314cf61b14be0b2366313 Mon Sep 17 00:00:00 2001 From: raymosun Date: Mon, 8 Jan 2024 17:02:28 -0800 Subject: [PATCH 08/22] Fix imports --- src/components/events/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/events/index.tsx b/src/components/events/index.tsx index 18664aff..31024178 100644 --- a/src/components/events/index.tsx +++ b/src/components/events/index.tsx @@ -1,2 +1,6 @@ +export { default as CalendarButtons } from './CalendarButtons'; +export { default as EventCard } from './EventCard'; export { default as EventCarousel } from './EventCarousel'; export { default as EventDisplay } from './EventDisplay'; +export { default as EventModal } from './EventModal'; +export { default as PointsDisplay } from './PointsDisplay'; From 461374f7a5a46b56c871b719e7bbb552783881d3 Mon Sep 17 00:00:00 2001 From: raymosun Date: Mon, 8 Jan 2024 20:21:28 -0800 Subject: [PATCH 09/22] Implement some requested changes --- .../profile/UserProfilePage/index.tsx | 80 +++++++++++++------ .../profile/UserProfilePage/style.module.scss | 24 ++++-- src/components/profile/index.ts | 4 +- src/lib/constants/ranks.json | 17 ++++ src/lib/utils.ts | 18 +---- src/pages/profile.tsx | 4 +- src/pages/profile/edit.tsx | 2 +- src/pages/u/[handle].tsx | 19 +++-- 8 files changed, 105 insertions(+), 63 deletions(-) create mode 100644 src/lib/constants/ranks.json diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index f167ee4b..ade67baa 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -1,9 +1,13 @@ import { Carousel, Typography } from '@/components/common'; import GifSafeImage from '@/components/common/GifSafeImage'; import { EventCard } from '@/components/events'; -import { config } from '@/lib'; -import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; -import { getLevel, getProfilePicture, getUserRank } from '@/lib/utils'; +import { config, showToast } from '@/lib'; +import { + PublicAttendance, + PublicUserSocialMedia, + type PublicProfile, +} from '@/lib/types/apiResponses'; +import { copy, getLevel, getProfilePicture, getUserRank } from '@/lib/utils'; import DevpostIcon from '@/public/assets/icons/devpost-icon.svg'; import EditIcon from '@/public/assets/icons/edit.svg'; import FacebookIcon from '@/public/assets/icons/facebook-icon.svg'; @@ -13,6 +17,7 @@ import LeaderboardIcon from '@/public/assets/icons/leaderboard-icon.svg'; import LinkedinIcon from '@/public/assets/icons/linkedin-icon.svg'; import MajorIcon from '@/public/assets/icons/major-icon.svg'; import ProfileIcon from '@/public/assets/icons/profile-icon.svg'; +import { Tooltip } from '@mui/material'; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { AiOutlineLink } from 'react-icons/ai'; @@ -20,7 +25,7 @@ import { IoMail } from 'react-icons/io5'; import styles from './style.module.scss'; export interface UserProfilePageProps { - user: PublicProfile; + handleUser: PublicProfile; isSignedInUser: boolean; signedInAttendances: PublicAttendance[]; attendances?: PublicAttendance[]; @@ -37,14 +42,14 @@ const socialMediaIcons = { }; export const UserProfilePage = ({ - user, + handleUser, attendances, signedInAttendances, isSignedInUser, }: UserProfilePageProps) => { // animate the progress bar const [progress, setProgress] = useState(0); - useEffect(() => setProgress(user.points % 100), [user.points]); + useEffect(() => setProgress(handleUser.points % 100), [handleUser.points]); return (
@@ -53,22 +58,38 @@ export const UserProfilePage = ({
- {`${user.firstName} ${user.lastName}`} - @{user.handle} + {`${handleUser.firstName} ${handleUser.lastName}`} + +
+ { + copy(window.location.href); + showToast('Profile link copied!'); + }} + > + @{handleUser.handle} + +
+
-
{getUserRank(user.points)}
+
{getUserRank(handleUser.points)}
  - {user.points.toLocaleString()} Leaderboard Points + {handleUser.points.toLocaleString()} All-Time Leaderboard Points
{isSignedInUser && ( @@ -84,20 +105,20 @@ export const UserProfilePage = ({
- {isSignedInUser ? 'My' : `${user.firstName}'s`} Progress + {isSignedInUser ? 'My' : `${handleUser.firstName}'s`} Progress
- Level {getLevel(user.points)} - {user.points % 100}/100 + Level {getLevel(handleUser.points)} + {handleUser.points % 100}/100
- {isSignedInUser ? 'You need' : `${user.firstName} needs`} {100 - (user.points % 100)} more - points to level up to + {isSignedInUser ? 'You need' : `${handleUser.firstName} needs`}{' '} + {100 - (handleUser.points % 100)} more points to level up to -  {getUserRank(user.points + 100)} +  {getUserRank(handleUser.points + 100)}
@@ -107,14 +128,16 @@ export const UserProfilePage = ({ About me
- Class of {user.graduationYear} + Class of {handleUser.graduationYear} - {user.major} + {handleUser.major}
- {user.userSocialMedia && + {handleUser.userSocialMedia && Object.entries(socialMediaIcons)?.map(([key, Icon]) => { - const matchingSocialMedia = user.userSocialMedia.find(obj => obj.type === key); + const matchingSocialMedia = handleUser.userSocialMedia.find( + (obj: PublicUserSocialMedia) => obj.type === key + ); if (!matchingSocialMedia) return null; return ( @@ -127,12 +150,12 @@ export const UserProfilePage = ({
Bio - {user.bio || Nothing here...} + {handleUser.bio || Nothing here...}
{isSignedInUser && (
- +
@@ -142,7 +165,14 @@ export const UserProfilePage = ({
{(attendances || isSignedInUser) && (
- Recently Attended Events + + Recently Attended Events + {!handleUser.isAttendancePublic && ( + +  (hidden for other users) + + )} + {(isSignedInUser ? signedInAttendances : (attendances as PublicAttendance[])).map( ({ event }) => ( diff --git a/src/components/profile/UserProfilePage/style.module.scss b/src/components/profile/UserProfilePage/style.module.scss index 1e629620..7dd7305b 100644 --- a/src/components/profile/UserProfilePage/style.module.scss +++ b/src/components/profile/UserProfilePage/style.module.scss @@ -46,6 +46,7 @@ } .cardName { + align-items: flex-start; display: flex; flex-direction: column; justify-content: center; @@ -53,6 +54,10 @@ @media (max-width: vars.$breakpoint-md) { align-items: center; flex-direction: column; + + h1 { + text-align: center; + } } } @@ -61,6 +66,7 @@ display: flex; flex-direction: column; flex-grow: 1; + gap: 0.5rem; justify-content: flex-end; @media (max-width: vars.$breakpoint-md) { @@ -73,15 +79,23 @@ display: flex; font-size: 1.25rem; font-weight: 500; - text-align: center; + + @media (max-width: vars.$breakpoint-md) { + color: var(--theme-text-on-background-2); + text-align: center + } } .points { color: var(--theme-text-on-background-1); font-size: 1.125rem; font-weight: 400; - line-height: 150%; - text-align: center; + line-height: 1.25; + text-align: end; + + @media (max-width: vars.$breakpoint-md) { + text-align: center; + } svg { height: 1.125rem; @@ -99,9 +113,9 @@ } .progressSection { - display: 'flex'; + display: flex; flex-direction: column; - gap: 1rem; + gap: 0.9375rem; .progressInfo { display: flex; diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index 5dde5283..b86fb02a 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -1,6 +1,6 @@ export { EditBlock, EditField, SingleField } from './EditField'; -export * from './HandleNotFound'; +export { HandleNotFound, type HandleNotFoundProps } from './HandleNotFound'; export { default as Preview } from './Preview'; export { default as SocialMediaIcon } from './SocialMediaIcon'; export { default as Switch } from './Switch'; -export * from './UserProfilePage'; +export { UserProfilePage, type UserProfilePageProps } from './UserProfilePage'; diff --git a/src/lib/constants/ranks.json b/src/lib/constants/ranks.json new file mode 100644 index 00000000..1ceb8add --- /dev/null +++ b/src/lib/constants/ranks.json @@ -0,0 +1,17 @@ +[ + "Factorial Flatbread", + "Exponential Eclair", + "Polynomial Pita", + "Cubic Croissant", + "Quadratic Qornbread", + "Linear Loaf", + "nlog Naan", + "Constant Cornbread", + "Binary Baguette", + "Blessed Boba", + "Super Snu", + "Soon(TM)", + "Later(TM)", + "Sometime(TM)", + "We Ran Out Of Ranks" +] diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 79773cb8..2807200c 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,5 @@ import defaultProfilePictures from '@/lib/constants/profilePictures'; +import ranks from '@/lib/constants/ranks.json'; import type { URL } from '@/lib/types'; import type { CustomErrorBody, @@ -111,23 +112,6 @@ export const getLevel = (points: number): number => { * @returns rank name */ export const getUserRank = (points: number): string => { - const ranks = [ - 'Factorial Flatbread', - 'Exponential Eclair', - 'Polynomial Pita', - 'Cubic Croissant', - 'Quadratic Qornbread', - 'Linear Loaf', - 'nlog Naan', - 'Constant Cornbread', - 'Binary Baguette', - 'Blessed Boba', - 'Super Snu', - 'Soon(TM)', - 'Later(TM)', - 'Sometime(TM)', - 'We Ran Out Of Ranks', - ] as const; const index = Math.min(ranks.length, getLevel(points)) - 1; return ranks[index] as string; }; diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index fe1a3d7f..3050a0ba 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -3,9 +3,7 @@ import { CookieService, PermissionService } from '@/lib/services'; import { CookieType } from '@/lib/types/enums'; import type { GetServerSideProps, NextPage } from 'next'; -const UserProfilePage: NextPage = () => { - return

Portal Profile Page

; -}; +const UserProfilePage: NextPage = () => null; export default UserProfilePage; diff --git a/src/pages/profile/edit.tsx b/src/pages/profile/edit.tsx index 2a47424b..0b7597b1 100644 --- a/src/pages/profile/edit.tsx +++ b/src/pages/profile/edit.tsx @@ -375,7 +375,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => {
-
+

About Me

diff --git a/src/pages/u/[handle].tsx b/src/pages/u/[handle].tsx index cb127e35..27716d64 100644 --- a/src/pages/u/[handle].tsx +++ b/src/pages/u/[handle].tsx @@ -32,27 +32,26 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res }); try { - const [user, signedInUser, signedInAttendances] = await Promise.all([ + const [handleUser, user, signedInAttendances] = await Promise.all([ UserAPI.getUserByHandle(token, handle).catch(() => null), UserAPI.getCurrentUser(token), UserAPI.getAttendancesForCurrentUser(token), ]); // render HandleNotFoundPage when user with handle is not retrieved - if (user === null) return { props: { handle } }; + if (handleUser === null) return { props: { handle } }; - const isSignedInUser = user.uuid === signedInUser.uuid; + const isSignedInUser = handleUser.uuid === user.uuid; - // UserProfilePage without separate user attendance - if (!user.isAttendancePublic || isSignedInUser) { - return { props: { user, isSignedInUser, signedInAttendances } }; - } + const attendances = + !handleUser.isAttendancePublic || isSignedInUser + ? null + : await UserAPI.getAttendancesForUserByUUID(token, user.uuid); - // UserProfilePage with separate user and signed-in attendances - const attendances = await UserAPI.getAttendancesForUserByUUID(token, user.uuid); + // render UserProfilePage return { props: { - user, + handleUser, isSignedInUser, signedInAttendances, attendances, From 27bbe33e3309ee40df265b0da388ac43afd7609c Mon Sep 17 00:00:00 2001 From: raymosun Date: Mon, 8 Jan 2024 21:05:28 -0800 Subject: [PATCH 10/22] Implement some requested changes (2) --- .../profile/UserProfilePage/index.tsx | 2 +- src/lib/constants/majors.json | 163 ------------------ src/lib/constants/majors.ts | 163 ++++++++++++++++++ src/lib/constants/ranks.json | 17 -- src/lib/constants/ranks.ts | 19 ++ src/lib/utils.ts | 8 +- src/pages/profile.tsx | 9 +- src/pages/profile/edit.tsx | 4 +- src/pages/register.tsx | 4 +- 9 files changed, 199 insertions(+), 190 deletions(-) delete mode 100644 src/lib/constants/majors.json create mode 100644 src/lib/constants/majors.ts delete mode 100644 src/lib/constants/ranks.json create mode 100644 src/lib/constants/ranks.ts diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index ade67baa..998a422b 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -115,7 +115,7 @@ export const UserProfilePage = ({
- {isSignedInUser ? 'You need' : `${handleUser.firstName} needs`}{' '} + {isSignedInUser ? 'You need ' : `${handleUser.firstName} needs `} {100 - (handleUser.points % 100)} more points to level up to  {getUserRank(handleUser.points + 100)} diff --git a/src/lib/constants/majors.json b/src/lib/constants/majors.json deleted file mode 100644 index 0b8e01ee..00000000 --- a/src/lib/constants/majors.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "majors": [ - "Aerospace Engineering", - "Anth (Conc Clim Chng&Humn Sol)", - "Anth (Conc Sociocultural Anth)", - "Anthropology(Conc in Archaeol)", - "Anthropology(Conc in Bio Anth)", - "Biochemistry", - "Biochemistry and Cell Biology", - "Bioengineering", - "Bioengineering (Biotechnology)", - "Bioengineering: Bioinformatics", - "Bioengineering: BioSystems", - "Biological Anthropology", - "Biology w/Spec Bioinformatics", - "BS PblHlth w/Con Biostatistics", - "BS PblHlth w/Con Clmt&EnvrnSci", - "BS PblHlth w/Con CmntyHlth Sci", - "BS PblHlth w/Con Epidemiology", - "BS PblHlth w/Con HlthPol&Mgmt", - "BS PblHlth w/Con Medicine Sci", - "Business Psychology", - "Chemical Engineering", - "Chemistry", - "Chinese Studies", - "Classical Studies", - "Cogn & Behavioral Neuroscience", - "Cognitive Science", - "Cogn Sci/Spec MachLrn & NuerCp", - "Cogn Sci w/Spec Clin Asp Cogn", - "Cogn Sci w/Spec Design & Inter", - "Cogn Sci w/Specializ Neurosci", - "Cogn Sci w/Spec Lang & Culture", - "Communication", - "Comp Sci w/Spec Bioinformatics", - "Computer Engineering", - "Computer Science", - "Critical Gender Studies", - "Dance", - "Data Science", - "Earth Sciences", - "Ecology, Behavior & Evolution", - "Economics", - "Education Sciences", - "Electrical Engin & Society", - "Electrical Engineering", - "Engineering Physics", - "Environmental Chemistry", - "Environmental Engineering", - "Environ Sys (Earth Sciences)", - "Environ Sys (Ecol,Behav&Evol)", - "Environ Sys(Environ Chemistry)", - "Environ Sys (Environ Policy)", - "Ethnic Studies", - "General Biology", - "General Physics", - "General Physics/Secondary Educ", - "German Studies", - "Global Health", - "Global South Studies", - "History", - "Human Biology", - "Human Developmental Sciences", - "HumDevSci w/Spec Eqty& Divrsty", - "HumDevSci w/Spec Healthy Aging", - "Interdisc Computing & the Arts", - "International Studies", - "International Studies-Anthro", - "International Studies-Econ", - "International Studies-History", - "International Studies-Intl Bus", - "International Studies-Linguist", - "International Studies-Lit", - "International Studies-Phil", - "International Studies-Poli Sci", - "International Studies-Sociol", - "Italian Studies", - "Japanese Studies", - "Jewish Studies", - "Joint Major Mathematics & Econ", - "Language Studies", - "Latin American Studies", - "Latin Am Stu (Con in Mexico)", - "Latin Am Stu (Con Mig&Brdr St)", - "Linguistics", - "Linguistics(Spec Cogn & Lang)", - "Linguistics(Spec Lang&Society)", - "Linguistics(Spec Spch&LangSci)", - "Literature/Writing", - "Literatures in English", - "Management Science", - "Marine Biology", - "Mathematics", - "Mathematics (Applied)", - "Mathematics-Computer Science", - "Mathematics-Scientif Computatn", - "Mathematics/Applied Science", - "Mathematics/Secondary Educ", - "Mechanical Engineering", - "MechEngW/Spec Cntrl & Robotics", - "MechEngW/SpecFluidMech&ThrmlSy", - "MechEngW/Spec Material Sci&Eng", - "MechEngW/SpecMechanics of Mat", - "MechEngW/SpecRnEnergy&EnvFlows", - "Microbiology", - "Molecular and Cell Biology", - "Molecular Biology", - "Molecular Synthesis", - "Music", - "Music Humanities", - "NanoEngineering", - "Neurobiology", - "Oceanic & Atmospheric Sciences", - "Pharmacological Chemistry", - "Philosophy", - "Physics", - "Physics-Biophysics", - "Physics w/Specializ Astrophys", - "Physics w/Specializ Earth Scis", - "Physics w/Specializ Mtrls Phys", - "Physiology & Neuroscience", - "Phys w/Spec Computational Phys", - "Political Science", - "Political Sci/Amer Politics", - "Political Sci/Compar Politics", - "Political Sci/Data Analytics", - "Political Sci/Intntl Relations", - "Political Sci/Political Theory", - "Political Sci/Public Law", - "Political Sci/Public Policy", - "Political Sci/Race Eth and Pol", - "Probability & Statistics", - "Psychology", - "Psychology/Clinical Psychology", - "Psychology/Cognitive Psych", - "Psychology/Developmental Psych", - "Psychology/Human Health", - "Psychology/Sensation&Perceptn", - "Psychology/Social Psychology", - "Public Health", - "Real Estate and Development", - "Russian EastEuro & Eurasian St", - "Sociology", - "Sociology-American Studies", - "Sociology-Culture/Communic", - "Sociology-Economy and Society", - "Sociology-International Stu", - "Sociology-Law and Society", - "Sociology-Science and Medicine", - "Sociology-Social Inequality", - "Spanish Literature", - "Speculative Design", - "Structural Engineering", - "Study of Religion", - "Theatre", - "Undeclared", - "Urban Studies and Planning", - "Visual Arts(Art Hist/Criticsm)", - "Visual Arts (Media)", - "Visual Arts (Studio)", - "World Literature and Culture" - ] -} diff --git a/src/lib/constants/majors.ts b/src/lib/constants/majors.ts new file mode 100644 index 00000000..6d85c8a4 --- /dev/null +++ b/src/lib/constants/majors.ts @@ -0,0 +1,163 @@ +const majors = [ + 'Aerospace Engineering', + 'Anth (Conc Clim Chng&Humn Sol)', + 'Anth (Conc Sociocultural Anth)', + 'Anthropology(Conc in Archaeol)', + 'Anthropology(Conc in Bio Anth)', + 'Biochemistry', + 'Biochemistry and Cell Biology', + 'Bioengineering', + 'Bioengineering (Biotechnology)', + 'Bioengineering: Bioinformatics', + 'Bioengineering: BioSystems', + 'Biological Anthropology', + 'Biology w/Spec Bioinformatics', + 'BS PblHlth w/Con Biostatistics', + 'BS PblHlth w/Con Clmt&EnvrnSci', + 'BS PblHlth w/Con CmntyHlth Sci', + 'BS PblHlth w/Con Epidemiology', + 'BS PblHlth w/Con HlthPol&Mgmt', + 'BS PblHlth w/Con Medicine Sci', + 'Business Psychology', + 'Chemical Engineering', + 'Chemistry', + 'Chinese Studies', + 'Classical Studies', + 'Cogn & Behavioral Neuroscience', + 'Cognitive Science', + 'Cogn Sci/Spec MachLrn & NuerCp', + 'Cogn Sci w/Spec Clin Asp Cogn', + 'Cogn Sci w/Spec Design & Inter', + 'Cogn Sci w/Specializ Neurosci', + 'Cogn Sci w/Spec Lang & Culture', + 'Communication', + 'Comp Sci w/Spec Bioinformatics', + 'Computer Engineering', + 'Computer Science', + 'Critical Gender Studies', + 'Dance', + 'Data Science', + 'Earth Sciences', + 'Ecology, Behavior & Evolution', + 'Economics', + 'Education Sciences', + 'Electrical Engin & Society', + 'Electrical Engineering', + 'Engineering Physics', + 'Environmental Chemistry', + 'Environmental Engineering', + 'Environ Sys (Earth Sciences)', + 'Environ Sys (Ecol,Behav&Evol)', + 'Environ Sys(Environ Chemistry)', + 'Environ Sys (Environ Policy)', + 'Ethnic Studies', + 'General Biology', + 'General Physics', + 'General Physics/Secondary Educ', + 'German Studies', + 'Global Health', + 'Global South Studies', + 'History', + 'Human Biology', + 'Human Developmental Sciences', + 'HumDevSci w/Spec Eqty& Divrsty', + 'HumDevSci w/Spec Healthy Aging', + 'Interdisc Computing & the Arts', + 'International Studies', + 'International Studies-Anthro', + 'International Studies-Econ', + 'International Studies-History', + 'International Studies-Intl Bus', + 'International Studies-Linguist', + 'International Studies-Lit', + 'International Studies-Phil', + 'International Studies-Poli Sci', + 'International Studies-Sociol', + 'Italian Studies', + 'Japanese Studies', + 'Jewish Studies', + 'Joint Major Mathematics & Econ', + 'Language Studies', + 'Latin American Studies', + 'Latin Am Stu (Con in Mexico)', + 'Latin Am Stu (Con Mig&Brdr St)', + 'Linguistics', + 'Linguistics(Spec Cogn & Lang)', + 'Linguistics(Spec Lang&Society)', + 'Linguistics(Spec Spch&LangSci)', + 'Literature/Writing', + 'Literatures in English', + 'Management Science', + 'Marine Biology', + 'Mathematics', + 'Mathematics (Applied)', + 'Mathematics-Computer Science', + 'Mathematics-Scientif Computatn', + 'Mathematics/Applied Science', + 'Mathematics/Secondary Educ', + 'Mechanical Engineering', + 'MechEngW/Spec Cntrl & Robotics', + 'MechEngW/SpecFluidMech&ThrmlSy', + 'MechEngW/Spec Material Sci&Eng', + 'MechEngW/SpecMechanics of Mat', + 'MechEngW/SpecRnEnergy&EnvFlows', + 'Microbiology', + 'Molecular and Cell Biology', + 'Molecular Biology', + 'Molecular Synthesis', + 'Music', + 'Music Humanities', + 'NanoEngineering', + 'Neurobiology', + 'Oceanic & Atmospheric Sciences', + 'Pharmacological Chemistry', + 'Philosophy', + 'Physics', + 'Physics-Biophysics', + 'Physics w/Specializ Astrophys', + 'Physics w/Specializ Earth Scis', + 'Physics w/Specializ Mtrls Phys', + 'Physiology & Neuroscience', + 'Phys w/Spec Computational Phys', + 'Political Science', + 'Political Sci/Amer Politics', + 'Political Sci/Compar Politics', + 'Political Sci/Data Analytics', + 'Political Sci/Intntl Relations', + 'Political Sci/Political Theory', + 'Political Sci/Public Law', + 'Political Sci/Public Policy', + 'Political Sci/Race Eth and Pol', + 'Probability & Statistics', + 'Psychology', + 'Psychology/Clinical Psychology', + 'Psychology/Cognitive Psych', + 'Psychology/Developmental Psych', + 'Psychology/Human Health', + 'Psychology/Sensation&Perceptn', + 'Psychology/Social Psychology', + 'Public Health', + 'Real Estate and Development', + 'Russian EastEuro & Eurasian St', + 'Sociology', + 'Sociology-American Studies', + 'Sociology-Culture/Communic', + 'Sociology-Economy and Society', + 'Sociology-International Stu', + 'Sociology-Law and Society', + 'Sociology-Science and Medicine', + 'Sociology-Social Inequality', + 'Spanish Literature', + 'Speculative Design', + 'Structural Engineering', + 'Study of Religion', + 'Theatre', + 'Undeclared', + 'Urban Studies and Planning', + 'Visual Arts(Art Hist/Criticsm)', + 'Visual Arts (Media)', + 'Visual Arts (Studio)', + 'World Literature and Culture', +]; + +export default majors; diff --git a/src/lib/constants/ranks.json b/src/lib/constants/ranks.json deleted file mode 100644 index 1ceb8add..00000000 --- a/src/lib/constants/ranks.json +++ /dev/null @@ -1,17 +0,0 @@ -[ - "Factorial Flatbread", - "Exponential Eclair", - "Polynomial Pita", - "Cubic Croissant", - "Quadratic Qornbread", - "Linear Loaf", - "nlog Naan", - "Constant Cornbread", - "Binary Baguette", - "Blessed Boba", - "Super Snu", - "Soon(TM)", - "Later(TM)", - "Sometime(TM)", - "We Ran Out Of Ranks" -] diff --git a/src/lib/constants/ranks.ts b/src/lib/constants/ranks.ts new file mode 100644 index 00000000..f2afc976 --- /dev/null +++ b/src/lib/constants/ranks.ts @@ -0,0 +1,19 @@ +const ranks = [ + 'Factorial Flatbread', + 'Exponential Eclair', + 'Polynomial Pita', + 'Cubic Croissant', + 'Quadratic Qornbread', + 'Linear Loaf', + 'nlog Naan', + 'Constant Cornbread', + 'Binary Baguette', + 'Blessed Boba', + 'Super Snu', + 'Soon(TM)', + 'Later(TM)', + 'Sometime(TM)', + 'We Ran Out Of Ranks', +]; + +export default ranks; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2807200c..5dabf68f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,5 @@ import defaultProfilePictures from '@/lib/constants/profilePictures'; -import ranks from '@/lib/constants/ranks.json'; +import ranks from '@/lib/constants/ranks'; import type { URL } from '@/lib/types'; import type { CustomErrorBody, @@ -8,7 +8,11 @@ import type { ValidatorError, } from '@/lib/types/apiResponses'; import NoImage from '@/public/assets/graphics/cat404.png'; -import { StaticImageData, StaticImport, StaticRequire } from 'next/dist/shared/lib/get-img-props'; +import { + type StaticImageData, + type StaticImport, + type StaticRequire, +} from 'next/dist/shared/lib/get-img-props'; import { useEffect, useState } from 'react'; /** diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 3050a0ba..0445452c 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -1,3 +1,5 @@ +import { config } from '@/lib'; +import { UserAPI } from '@/lib/api'; import withAccessType from '@/lib/hoc/withAccessType'; import { CookieService, PermissionService } from '@/lib/services'; import { CookieType } from '@/lib/types/enums'; @@ -8,12 +10,13 @@ const UserProfilePage: NextPage = () => null; export default UserProfilePage; const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => { - const user = JSON.parse(CookieService.getServerCookie(CookieType.USER, { req, res })); + const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res }); + const user = await UserAPI.getCurrentUser(token); return { redirect: { - destination: `/u/${user.handle}`, - permanent: true, + destination: `${config.userProfileRoute}/${user.handle}`, + permanent: false, }, }; }; diff --git a/src/pages/profile/edit.tsx b/src/pages/profile/edit.tsx index 0b7597b1..4cc17f68 100644 --- a/src/pages/profile/edit.tsx +++ b/src/pages/profile/edit.tsx @@ -10,7 +10,7 @@ import { } from '@/components/profile'; import { config, showToast } from '@/lib'; import { AuthAPI, ResumeAPI, UserAPI } from '@/lib/api'; -import majors from '@/lib/constants/majors.json'; +import majors from '@/lib/constants/majors'; import socialMediaTypes from '@/lib/constants/socialMediaTypes'; import withAccessType from '@/lib/hoc/withAccessType'; import { CookieService, PermissionService } from '@/lib/services'; @@ -384,7 +384,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => { { } name="major" - options={data.majors} + options={majors} element="select" placeholder="Major" error={errors.major} From ac96065b522bce3b5003849a5205edc4f9f21337 Mon Sep 17 00:00:00 2001 From: raymosun Date: Mon, 8 Jan 2024 21:17:40 -0800 Subject: [PATCH 11/22] Fix a color issue --- src/components/profile/UserProfilePage/style.module.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/profile/UserProfilePage/style.module.scss b/src/components/profile/UserProfilePage/style.module.scss index 7dd7305b..a9c573db 100644 --- a/src/components/profile/UserProfilePage/style.module.scss +++ b/src/components/profile/UserProfilePage/style.module.scss @@ -71,6 +71,7 @@ @media (max-width: vars.$breakpoint-md) { align-items: center; + color: var(--theme-text-on-background-2); flex-direction: column; } @@ -81,7 +82,6 @@ font-weight: 500; @media (max-width: vars.$breakpoint-md) { - color: var(--theme-text-on-background-2); text-align: center } } From 103f6bb9d9308985f454f36b62fb9f02ed275730 Mon Sep 17 00:00:00 2001 From: Faris Ashai Date: Mon, 8 Jan 2024 22:11:04 -0800 Subject: [PATCH 12/22] fix tiny bugs --- src/components/profile/Preview/style.module.scss | 2 ++ src/pages/profile.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/profile/Preview/style.module.scss b/src/components/profile/Preview/style.module.scss index 4e26475c..abdd0247 100644 --- a/src/components/profile/Preview/style.module.scss +++ b/src/components/profile/Preview/style.module.scss @@ -2,7 +2,9 @@ align-items: center; display: flex; flex-direction: column; + position: sticky; text-align: center; + top: 4rem; .pfp { border-radius: 50%; diff --git a/src/pages/profile.tsx b/src/pages/profile.tsx index 0445452c..b30c533d 100644 --- a/src/pages/profile.tsx +++ b/src/pages/profile.tsx @@ -15,7 +15,7 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => { return { redirect: { - destination: `${config.userProfileRoute}/${user.handle}`, + destination: `${config.userProfileRoute}${user.handle}`, permanent: false, }, }; From 8a0198fd280f38cb33d2fb9007c79be99804e06c Mon Sep 17 00:00:00 2001 From: raymosun Date: Tue, 9 Jan 2024 16:46:37 -0800 Subject: [PATCH 13/22] Implement some requested changes (3) --- src/components/profile/Preview/index.tsx | 4 +- .../profile/Preview/style.module.scss | 1 + .../index.tsx | 4 +- .../profile/UserProfilePage/index.tsx | 53 ++++++------------- .../profile/UserProfilePage/style.module.scss | 14 +++-- src/components/profile/index.ts | 2 +- src/lib/api/EventAPI.ts | 14 ----- src/lib/utils.ts | 7 +++ src/pages/events.tsx | 4 +- src/pages/index.tsx | 2 +- src/pages/profile/edit.tsx | 3 +- src/pages/u/[handle].tsx | 18 ++++--- 12 files changed, 54 insertions(+), 72 deletions(-) rename src/components/profile/{HandleNotFound => UserHandleNotFound}/index.tsx (83%) diff --git a/src/components/profile/Preview/index.tsx b/src/components/profile/Preview/index.tsx index 1fa1cb80..856c970d 100644 --- a/src/components/profile/Preview/index.tsx +++ b/src/components/profile/Preview/index.tsx @@ -3,11 +3,9 @@ import GifSafeImage from '@/components/common/GifSafeImage'; import SocialMediaIcon from '@/components/profile/SocialMediaIcon'; import { PublicProfile } from '@/lib/types/apiResponses'; import { SocialMediaType } from '@/lib/types/enums'; -import { getLevel, getProfilePicture } from '@/lib/utils'; +import { fixUrl, getLevel, getProfilePicture } from '@/lib/utils'; import styles from './style.module.scss'; -const fixUrl = (url: string) => (url.includes('://') ? url : `http://${url}`); - interface PreviewStatProps { title: string; value: number; diff --git a/src/components/profile/Preview/style.module.scss b/src/components/profile/Preview/style.module.scss index 4e26475c..25bcd1b8 100644 --- a/src/components/profile/Preview/style.module.scss +++ b/src/components/profile/Preview/style.module.scss @@ -33,6 +33,7 @@ .bio { white-space: pre-wrap; + word-break: break-word; } .socials { diff --git a/src/components/profile/HandleNotFound/index.tsx b/src/components/profile/UserHandleNotFound/index.tsx similarity index 83% rename from src/components/profile/HandleNotFound/index.tsx rename to src/components/profile/UserHandleNotFound/index.tsx index 74c3edd3..4bc258d8 100644 --- a/src/components/profile/HandleNotFound/index.tsx +++ b/src/components/profile/UserHandleNotFound/index.tsx @@ -3,11 +3,11 @@ import { Typography, VerticalForm } from '@/components/common'; import Cat404 from '@/public/assets/graphics/cat404.png'; import Image from 'next/image'; -export interface HandleNotFoundProps { +export interface UserHandleNotFoundProps { handle: string; } -export const HandleNotFound = ({ handle }: HandleNotFoundProps) => ( +export const UserHandleNotFound = ({ handle }: UserHandleNotFoundProps) => ( No user with handle ‘{handle}’ was found. diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index 998a422b..c2a1260b 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -1,27 +1,18 @@ import { Carousel, Typography } from '@/components/common'; import GifSafeImage from '@/components/common/GifSafeImage'; import { EventCard } from '@/components/events'; +import SocialMediaIcon from '@/components/profile/SocialMediaIcon'; import { config, showToast } from '@/lib'; -import { - PublicAttendance, - PublicUserSocialMedia, - type PublicProfile, -} from '@/lib/types/apiResponses'; -import { copy, getLevel, getProfilePicture, getUserRank } from '@/lib/utils'; -import DevpostIcon from '@/public/assets/icons/devpost-icon.svg'; +import { PublicAttendance, type PublicProfile } from '@/lib/types/apiResponses'; +import { SocialMediaType } from '@/lib/types/enums'; +import { copy, fixUrl, getLevel, getProfilePicture, getUserRank } from '@/lib/utils'; import EditIcon from '@/public/assets/icons/edit.svg'; -import FacebookIcon from '@/public/assets/icons/facebook-icon.svg'; -import GithubIcon from '@/public/assets/icons/github-icon.svg'; -import InstagramIcon from '@/public/assets/icons/instagram.svg'; import LeaderboardIcon from '@/public/assets/icons/leaderboard-icon.svg'; -import LinkedinIcon from '@/public/assets/icons/linkedin-icon.svg'; import MajorIcon from '@/public/assets/icons/major-icon.svg'; import ProfileIcon from '@/public/assets/icons/profile-icon.svg'; import { Tooltip } from '@mui/material'; import Link from 'next/link'; import { useEffect, useState } from 'react'; -import { AiOutlineLink } from 'react-icons/ai'; -import { IoMail } from 'react-icons/io5'; import styles from './style.module.scss'; export interface UserProfilePageProps { @@ -31,16 +22,6 @@ export interface UserProfilePageProps { attendances?: PublicAttendance[]; } -const socialMediaIcons = { - LINKEDIN: LinkedinIcon, - GITHUB: GithubIcon, - DEVPOST: DevpostIcon, - PORTFOLIO: AiOutlineLink, - FACEBOOK: FacebookIcon, - INSTAGRAM: InstagramIcon, - EMAIL: IoMail, -}; - export const UserProfilePage = ({ handleUser, attendances, @@ -89,7 +70,7 @@ export const UserProfilePage = ({
{getUserRank(handleUser.points)}
  - {handleUser.points.toLocaleString()} All-Time Leaderboard Points + {handleUser.points.toLocaleString()} Leaderboard Points
{isSignedInUser && ( @@ -133,18 +114,18 @@ export const UserProfilePage = ({ {handleUser.major}
diff --git a/src/components/profile/UserProfilePage/style.module.scss b/src/components/profile/UserProfilePage/style.module.scss index a9c573db..9f0526fa 100644 --- a/src/components/profile/UserProfilePage/style.module.scss +++ b/src/components/profile/UserProfilePage/style.module.scss @@ -55,7 +55,7 @@ align-items: center; flex-direction: column; - h1 { + h1, div { text-align: center; } } @@ -80,9 +80,11 @@ display: flex; font-size: 1.25rem; font-weight: 500; + margin-top: calc(2.375rem + 0.5rem); // give space for edit button @media (max-width: vars.$breakpoint-md) { - text-align: center + margin-top: 0; + text-align: center; } } @@ -99,7 +101,8 @@ svg { height: 1.125rem; - width: 1.125rem; + transform: translateY(calc((1.125rem * 1.25) - 1.125rem) * 0.5); // center the trophy icon + width: 1.125rem } } } @@ -124,7 +127,7 @@ } .progressBar { - background-color: var(--theme-text-on-background-3); + background-color: var(--theme-accent-line-2); border-radius: 0.5rem; height: 1.25rem; width: 100%; @@ -156,6 +159,7 @@ } .aboutMeSection { + align-items: center; display: grid; gap: 0.56rem; grid-template-columns: auto 1fr; @@ -177,6 +181,8 @@ display: flex; flex-direction: column; gap: 0.5rem; + white-space: pre-wrap; + word-break: break-word; } } diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts index b86fb02a..af7d3385 100644 --- a/src/components/profile/index.ts +++ b/src/components/profile/index.ts @@ -1,6 +1,6 @@ export { EditBlock, EditField, SingleField } from './EditField'; -export { HandleNotFound, type HandleNotFoundProps } from './HandleNotFound'; export { default as Preview } from './Preview'; export { default as SocialMediaIcon } from './SocialMediaIcon'; export { default as Switch } from './Switch'; +export { UserHandleNotFound, type UserHandleNotFoundProps } from './UserHandleNotFound'; export { UserProfilePage, type UserProfilePageProps } from './UserProfilePage'; diff --git a/src/lib/api/EventAPI.ts b/src/lib/api/EventAPI.ts index 5a289de7..ad92f813 100644 --- a/src/lib/api/EventAPI.ts +++ b/src/lib/api/EventAPI.ts @@ -5,12 +5,10 @@ import { AttendEventResponse, CreateEventResponse, GetAllEventsResponse, - GetAttendancesForUserResponse, GetFutureEventsResponse, GetOneEventResponse, GetPastEventsResponse, PatchEventResponse, - PublicAttendance, PublicEvent, } from '@/lib/types/apiResponses'; import axios from 'axios'; @@ -72,18 +70,6 @@ export const getAllEvents = async (): Promise => { return response.data.events; }; -export const getAttendancesForUser = async (token: string): Promise => { - const requestUrl = `${config.api.baseUrl}${config.api.endpoints.attendance.attendance}`; - - const response = await axios.get(requestUrl, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - return response.data.attendances; -}; - export const attendEvent = async ( token: string, attendanceCode: string diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5dabf68f..4d0dd14d 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -273,3 +273,10 @@ export const getDefaultMerchItemPhoto = (item: PublicMerchItem | undefined): str } return NoImage.src; }; + +/** + * Prepend 'http://' to a url if a protocol isn't specified + * @param url url to be fixed + * @returns url begnning with http:// + */ +export const fixUrl = (url: string) => (url.includes('://') ? url : `http://${url}`); diff --git a/src/pages/events.tsx b/src/pages/events.tsx index c6cc0f84..900a8790 100644 --- a/src/pages/events.tsx +++ b/src/pages/events.tsx @@ -1,6 +1,6 @@ import { Dropdown, PaginationControls, Typography } from '@/components/common'; import { EventDisplay } from '@/components/events'; -import { EventAPI } from '@/lib/api'; +import { EventAPI, UserAPI } from '@/lib/api'; import withAccessType from '@/lib/hoc/withAccessType'; import { CookieService, PermissionService } from '@/lib/services'; import type { PublicAttendance, PublicEvent } from '@/lib/types/apiResponses'; @@ -173,7 +173,7 @@ export default EventsPage; const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => { const authToken = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res }); const events = await EventAPI.getAllEvents(); - const attendances = await EventAPI.getAttendancesForUser(authToken); + const attendances = await UserAPI.getAttendancesForCurrentUser(authToken); return { props: { events, attendances } }; }; diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 568b7d36..65f6f9df 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -126,7 +126,7 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ req, res, query }) = // After that, fetch the other API calls. const eventsPromise = EventAPI.getAllEvents(); - const attendancesPromise = EventAPI.getAttendancesForUser(authToken); + const attendancesPromise = UserAPI.getAttendancesForCurrentUser(authToken); const userPromise = UserAPI.getCurrentUser(authToken); const [events, attendances, user] = await Promise.all([ diff --git a/src/pages/profile/edit.tsx b/src/pages/profile/edit.tsx index 4cc17f68..84bccb0d 100644 --- a/src/pages/profile/edit.tsx +++ b/src/pages/profile/edit.tsx @@ -36,7 +36,8 @@ function reportError(title: string, error: unknown) { } function fixUrl(input: string, prefix?: string): string { - if (!input || input.startsWith('https://')) { + // Return input as-is if it's blank or includes a protocol + if (!input || input.includes('://')) { return input; } // Encourage https:// diff --git a/src/pages/u/[handle].tsx b/src/pages/u/[handle].tsx index 27716d64..31a85480 100644 --- a/src/pages/u/[handle].tsx +++ b/src/pages/u/[handle].tsx @@ -1,6 +1,6 @@ import { - HandleNotFound, - HandleNotFoundProps, + UserHandleNotFound, + UserHandleNotFoundProps, UserProfilePage, UserProfilePageProps, } from '@/components/profile'; @@ -8,18 +8,19 @@ import { config } from '@/lib'; import { UserAPI } from '@/lib/api'; import withAccessType from '@/lib/hoc/withAccessType'; import { CookieService, PermissionService } from '@/lib/services'; +import { setServerCookie } from '@/lib/services/CookieService'; import { CookieType } from '@/lib/types/enums'; import type { GetServerSideProps } from 'next/types'; -type UserHandlePageProps = HandleNotFoundProps | UserProfilePageProps; +type UserHandlePageProps = UserHandleNotFoundProps | UserProfilePageProps; -const isHandleNotFound = (props: UserHandlePageProps): props is HandleNotFoundProps => +const isUserHandleNotFound = (props: UserHandlePageProps): props is UserHandleNotFoundProps => 'handle' in props; const UserHandlePage = (props: UserHandlePageProps) => { - if (isHandleNotFound(props)) { + if (isUserHandleNotFound(props)) { const { handle } = props; - return ; + return ; } return ; @@ -37,8 +38,9 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) UserAPI.getCurrentUser(token), UserAPI.getAttendancesForCurrentUser(token), ]); + setServerCookie(CookieType.USER, JSON.stringify(user), { req, res }); - // render HandleNotFoundPage when user with handle is not retrieved + // render UserHandleNotFoundPage when user with handle is not retrieved if (handleUser === null) return { props: { handle } }; const isSignedInUser = handleUser.uuid === user.uuid; @@ -46,7 +48,7 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) const attendances = !handleUser.isAttendancePublic || isSignedInUser ? null - : await UserAPI.getAttendancesForUserByUUID(token, user.uuid); + : (await UserAPI.getAttendancesForUserByUUID(token, user.uuid)).slice(0, 10); // render UserProfilePage return { From 5d53c744d6b16a9436d668f7b4063fc681a19be9 Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 11 Jan 2024 20:46:26 -0800 Subject: [PATCH 14/22] Add one edit profile test --- cypress/e2e/pages/edit-profile.cy.ts | 35 ++++++++++++++++++++ cypress/fixtures/profile.json | 7 ++++ cypress/support/commands.ts | 49 ++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+) create mode 100644 cypress/e2e/pages/edit-profile.cy.ts create mode 100644 cypress/fixtures/profile.json diff --git a/cypress/e2e/pages/edit-profile.cy.ts b/cypress/e2e/pages/edit-profile.cy.ts new file mode 100644 index 00000000..d68d089e --- /dev/null +++ b/cypress/e2e/pages/edit-profile.cy.ts @@ -0,0 +1,35 @@ +/// + +describe('Forgot Password Page', () => { + beforeEach(() => { + cy.login('standard'); + cy.location('pathname').should('equal', '/'); + cy.visit('/profile/edit'); + cy.location('pathname').should('equal', '/profile/edit'); + }); + + it('Should update profile preview', () => { + // write something new to make sure it can save + cy.typeInForm('First', new Date().toISOString()); + cy.get('button').contains('Save').click(); + + cy.fixture('profile').then(profile => { + const { first, last, bio, major, year } = profile; + + cy.typeInForm('First', first); + cy.typeInForm('Last', last); + cy.selectInForm('Major', major); + cy.selectInForm('Graduation Year', year); + cy.typeInForm('Biography', bio); + cy.get('button').contains('Save').click(); + + cy.get('.Toastify').contains('Changes saved!').should('exist'); + + cy.get('section:contains("Current Profile")').within(() => { + Object.values(profile).forEach((value: string | number) => { + cy.contains(value).should('exist'); + }); + }); + }); + }); +}); diff --git a/cypress/fixtures/profile.json b/cypress/fixtures/profile.json new file mode 100644 index 00000000..5f6df697 --- /dev/null +++ b/cypress/fixtures/profile.json @@ -0,0 +1,7 @@ +{ + "first": "John", + "last": "Doe", + "bio": "I am a testing account.", + "major": "Computer Engineering", + "year": "2026" +} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index cb0ff5c3..a3473cb6 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1 +1,50 @@ +declare global { + namespace Cypress { + interface Chainable { + /** + * Log in as the specified user, pulling details from accounts.json + * @example cy.login('standard') + */ + login(account: string): Chainable; + + /** + * Type text into a form input or textarea under specified label + * @param label - text of label that the input is a child of + * @param value - text to be typed into input + */ + typeInForm(label: string, value: string): Chainable; + + /** + * Select the given option in a select under specified label + * @param label - text of label that the select is a child of + * @param value - option to be selected + */ + selectInForm(label: string, value: string): Chainable; + } + } +} + +Cypress.Commands.add('login', (account: string) => { + cy.fixture('accounts.json').then(accs => { + if (!(account in accs)) + throw new Error(`Account '${account}' isn't specified in \`accounts.json\``); + const { email, password } = accs[account]; + + cy.visit('/login'); + cy.get('input[name="email"]').type(email); + cy.get('input[name="password"]').type(password); + cy.get('button').contains('Sign In').click(); + }); +}); + +Cypress.Commands.add('typeInForm', (label: string, value: string) => { + cy.get(`label:contains("${label}") input, label:contains("${label}") textarea`).as('input'); + cy.get('@input').clear(); + cy.get('@input').type(value as string); +}); + +Cypress.Commands.add('selectInForm', (label: string, value: string | number) => { + cy.get(`label:contains("${label}") select`).select(value); +}); + export {}; From 2c51159cdd6872e1dc1967a50179fe1cfcda22ce Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 11 Jan 2024 21:08:52 -0800 Subject: [PATCH 15/22] Some small fixes --- src/components/profile/UserProfilePage/index.tsx | 2 +- src/components/profile/UserProfilePage/style.module.scss | 4 ++++ .../profile/UserProfilePage/style.module.scss.d.ts | 1 + src/pages/events.tsx | 6 ++++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index c2a1260b..a7e14d09 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -53,7 +53,7 @@ export const UserProfilePage = ({ component="h1" >{`${handleUser.firstName} ${handleUser.lastName}`} -
+
{ diff --git a/src/components/profile/UserProfilePage/style.module.scss b/src/components/profile/UserProfilePage/style.module.scss index 9f0526fa..4bafbbca 100644 --- a/src/components/profile/UserProfilePage/style.module.scss +++ b/src/components/profile/UserProfilePage/style.module.scss @@ -59,6 +59,10 @@ text-align: center; } } + + .handle { + cursor: pointer; + } } .cardRank { diff --git a/src/components/profile/UserProfilePage/style.module.scss.d.ts b/src/components/profile/UserProfilePage/style.module.scss.d.ts index f328d500..dffc59c4 100644 --- a/src/components/profile/UserProfilePage/style.module.scss.d.ts +++ b/src/components/profile/UserProfilePage/style.module.scss.d.ts @@ -8,6 +8,7 @@ export type Styles = { cardRank: string; cardWrapper: string; editWrapper: string; + handle: string; icon: string; inner: string; points: string; diff --git a/src/pages/events.tsx b/src/pages/events.tsx index 900a8790..cf2853be 100644 --- a/src/pages/events.tsx +++ b/src/pages/events.tsx @@ -172,8 +172,10 @@ export default EventsPage; const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => { const authToken = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res }); - const events = await EventAPI.getAllEvents(); - const attendances = await UserAPI.getAttendancesForCurrentUser(authToken); + const [events, attendances] = await Promise.all([ + EventAPI.getAllEvents(), + UserAPI.getAttendancesForCurrentUser(authToken), + ]); return { props: { events, attendances } }; }; From e6e6873877b5d22d365c9dc0ee9a8e7b10863e92 Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 11 Jan 2024 21:12:30 -0800 Subject: [PATCH 16/22] Update imports for GifSafeImage --- src/components/leaderboard/LeaderboardRow/index.tsx | 2 +- src/components/leaderboard/TopThreeCard/index.tsx | 2 +- src/components/profile/Preview/index.tsx | 3 +-- src/components/profile/UserProfilePage/index.tsx | 3 +-- src/pages/profile/edit.tsx | 3 +-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/leaderboard/LeaderboardRow/index.tsx b/src/components/leaderboard/LeaderboardRow/index.tsx index db101939..b857cac8 100644 --- a/src/components/leaderboard/LeaderboardRow/index.tsx +++ b/src/components/leaderboard/LeaderboardRow/index.tsx @@ -1,4 +1,4 @@ -import GifSafeImage from '@/components/common/GifSafeImage'; +import { GifSafeImage } from '@/components/common'; import Link from 'next/link'; import { useEffect, useRef } from 'react'; import styles from './style.module.scss'; diff --git a/src/components/leaderboard/TopThreeCard/index.tsx b/src/components/leaderboard/TopThreeCard/index.tsx index 44ccb159..f434ec88 100644 --- a/src/components/leaderboard/TopThreeCard/index.tsx +++ b/src/components/leaderboard/TopThreeCard/index.tsx @@ -1,4 +1,4 @@ -import GifSafeImage from '@/components/common/GifSafeImage'; +import { GifSafeImage } from '@/components/common'; import { trim } from '@/lib/utils'; import Link from 'next/link'; import styles from './style.module.scss'; diff --git a/src/components/profile/Preview/index.tsx b/src/components/profile/Preview/index.tsx index 856c970d..34559c5e 100644 --- a/src/components/profile/Preview/index.tsx +++ b/src/components/profile/Preview/index.tsx @@ -1,5 +1,4 @@ -import { Typography } from '@/components/common'; -import GifSafeImage from '@/components/common/GifSafeImage'; +import { GifSafeImage, Typography } from '@/components/common'; import SocialMediaIcon from '@/components/profile/SocialMediaIcon'; import { PublicProfile } from '@/lib/types/apiResponses'; import { SocialMediaType } from '@/lib/types/enums'; diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index a7e14d09..46b8a8c2 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -1,5 +1,4 @@ -import { Carousel, Typography } from '@/components/common'; -import GifSafeImage from '@/components/common/GifSafeImage'; +import { Carousel, GifSafeImage, Typography } from '@/components/common'; import { EventCard } from '@/components/events'; import SocialMediaIcon from '@/components/profile/SocialMediaIcon'; import { config, showToast } from '@/lib'; diff --git a/src/pages/profile/edit.tsx b/src/pages/profile/edit.tsx index 84bccb0d..37f40d92 100644 --- a/src/pages/profile/edit.tsx +++ b/src/pages/profile/edit.tsx @@ -1,5 +1,4 @@ -import { Cropper } from '@/components/common'; -import GifSafeImage from '@/components/common/GifSafeImage'; +import { Cropper, GifSafeImage } from '@/components/common'; import { EditBlock, EditField, From 010b8a8d390645a2b498277b271b3b5aa502c69c Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 18 Jan 2024 20:45:26 -0800 Subject: [PATCH 17/22] Fix sticky profile preview --- src/components/profile/Preview/style.module.scss | 3 +-- src/styles/pages/profile/edit.module.scss | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/profile/Preview/style.module.scss b/src/components/profile/Preview/style.module.scss index 848b5897..0ad749a1 100644 --- a/src/components/profile/Preview/style.module.scss +++ b/src/components/profile/Preview/style.module.scss @@ -2,9 +2,8 @@ align-items: center; display: flex; flex-direction: column; - position: sticky; + text-align: center; - top: 4rem; .pfp { border-radius: 50%; diff --git a/src/styles/pages/profile/edit.module.scss b/src/styles/pages/profile/edit.module.scss index 0f00b6f5..d1f2c4a3 100644 --- a/src/styles/pages/profile/edit.module.scss +++ b/src/styles/pages/profile/edit.module.scss @@ -33,9 +33,14 @@ .columnLeft { border-right: 0.5px solid vars.$light-primary-2; flex: none; + margin-bottom: -2rem; // extend column all the way to the bottom margin-right: 2rem; + max-height: calc(100vh - 4rem); // subtract height of navbar + overflow-y: auto; padding: 2rem; padding-left: 0; + position: sticky; + top: 4rem; width: 24rem; h2 { From 0f75380865ba09dfa90c3073412252ac2d6f79b9 Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 18 Jan 2024 20:55:55 -0800 Subject: [PATCH 18/22] Replace imported icons with svgs --- public/assets/icons/email-solid.svg | 1 + public/assets/icons/link-icon.svg | 1 + public/assets/icons/twitter.svg | 1 + src/lib/constants/socialMediaTypes.ts | 27 +++++++++++++++------------ 4 files changed, 18 insertions(+), 12 deletions(-) create mode 100644 public/assets/icons/email-solid.svg create mode 100644 public/assets/icons/link-icon.svg create mode 100644 public/assets/icons/twitter.svg diff --git a/public/assets/icons/email-solid.svg b/public/assets/icons/email-solid.svg new file mode 100644 index 00000000..a0caf3fd --- /dev/null +++ b/public/assets/icons/email-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/link-icon.svg b/public/assets/icons/link-icon.svg new file mode 100644 index 00000000..ea1335c6 --- /dev/null +++ b/public/assets/icons/link-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/icons/twitter.svg b/public/assets/icons/twitter.svg new file mode 100644 index 00000000..6ec13e8b --- /dev/null +++ b/public/assets/icons/twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/lib/constants/socialMediaTypes.ts b/src/lib/constants/socialMediaTypes.ts index bc54883b..b0e142b6 100644 --- a/src/lib/constants/socialMediaTypes.ts +++ b/src/lib/constants/socialMediaTypes.ts @@ -1,12 +1,15 @@ import { SocialMediaType } from '@/lib/types/enums'; import DevpostIcon from '@/public/assets/icons/devpost-icon.svg'; -import { IconType } from 'react-icons'; -import { AiOutlineLink } from 'react-icons/ai'; -import { BsFacebook, BsGithub, BsInstagram, BsLinkedin, BsTwitter } from 'react-icons/bs'; -import { IoMail } from 'react-icons/io5'; +import EmailIcon from '@/public/assets/icons/email-solid.svg'; +import FacebookIcon from '@/public/assets/icons/facebook-icon.svg'; +import GithubIcon from '@/public/assets/icons/github-icon.svg'; +import InstagramIcon from '@/public/assets/icons/instagram.svg'; +import LinkIcon from '@/public/assets/icons/link-icon.svg'; +import LinkedInIcon from '@/public/assets/icons/linkedin-icon.svg'; +import TwitterIcon from '@/public/assets/icons/twitter.svg'; interface SocialMediaInfo { - icon: IconType; + icon: string; label: string; domain?: string; example: string; @@ -20,41 +23,41 @@ const socialMediaTypes: Record = { example: 'devpost.com/', }, [SocialMediaType.EMAIL]: { - icon: IoMail, + icon: EmailIcon, label: 'Email', example: 'you@example.com', }, [SocialMediaType.FACEBOOK]: { - icon: BsFacebook, + icon: FacebookIcon, label: 'Facebook', domain: 'facebook.com', example: 'facebook.com/', }, [SocialMediaType.GITHUB]: { - icon: BsGithub, + icon: GithubIcon, label: 'GitHub', domain: 'github.com', example: 'github.com/', }, [SocialMediaType.INSTAGRAM]: { - icon: BsInstagram, + icon: InstagramIcon, label: 'Instagram', domain: 'instagram.com', example: 'instagram.com/', }, [SocialMediaType.LINKEDIN]: { - icon: BsLinkedin, + icon: LinkedInIcon, label: 'LinkedIn', domain: 'linkedin.com/in', example: 'linkedin.com/in/', }, [SocialMediaType.PORTFOLIO]: { - icon: AiOutlineLink, + icon: LinkIcon, label: 'Portfolio', example: 'example.com', }, [SocialMediaType.TWITTER]: { - icon: BsTwitter, + icon: TwitterIcon, label: 'Twitter', domain: 'twitter.com', example: 'twitter.com/', From 3114ade60a683041b192e9763bc00e94a88c5709 Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 18 Jan 2024 21:01:05 -0800 Subject: [PATCH 19/22] Fix types --- src/lib/constants/socialMediaTypes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/constants/socialMediaTypes.ts b/src/lib/constants/socialMediaTypes.ts index b0e142b6..c2141cc2 100644 --- a/src/lib/constants/socialMediaTypes.ts +++ b/src/lib/constants/socialMediaTypes.ts @@ -7,9 +7,10 @@ import InstagramIcon from '@/public/assets/icons/instagram.svg'; import LinkIcon from '@/public/assets/icons/link-icon.svg'; import LinkedInIcon from '@/public/assets/icons/linkedin-icon.svg'; import TwitterIcon from '@/public/assets/icons/twitter.svg'; +import { ComponentType, SVGProps } from 'react'; interface SocialMediaInfo { - icon: string; + icon: ComponentType>; label: string; domain?: string; example: string; From ba32f558a1cf3de08eae307d5b96e1216d5e0579 Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 18 Jan 2024 21:11:31 -0800 Subject: [PATCH 20/22] Small fixes --- cypress/e2e/pages/edit-profile.cy.ts | 2 +- src/components/profile/UserProfilePage/index.tsx | 2 +- .../profile/UserProfilePage/style.module.scss | 11 ++++++++--- .../profile/UserProfilePage/style.module.scss.d.ts | 1 + 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/cypress/e2e/pages/edit-profile.cy.ts b/cypress/e2e/pages/edit-profile.cy.ts index d68d089e..374d5678 100644 --- a/cypress/e2e/pages/edit-profile.cy.ts +++ b/cypress/e2e/pages/edit-profile.cy.ts @@ -1,6 +1,6 @@ /// -describe('Forgot Password Page', () => { +describe('Edit Profile Page', () => { beforeEach(() => { cy.login('standard'); cy.location('pathname').should('equal', '/'); diff --git a/src/components/profile/UserProfilePage/index.tsx b/src/components/profile/UserProfilePage/index.tsx index 46b8a8c2..ad1d5a60 100644 --- a/src/components/profile/UserProfilePage/index.tsx +++ b/src/components/profile/UserProfilePage/index.tsx @@ -112,7 +112,7 @@ export const UserProfilePage = ({ {handleUser.major}
-
+
{handleUser.userSocialMedia?.map(social => ( Date: Thu, 18 Jan 2024 21:18:17 -0800 Subject: [PATCH 21/22] Small fix --- src/pages/u/[handle].tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/u/[handle].tsx b/src/pages/u/[handle].tsx index 31a85480..d6cd4390 100644 --- a/src/pages/u/[handle].tsx +++ b/src/pages/u/[handle].tsx @@ -45,10 +45,9 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) const isSignedInUser = handleUser.uuid === user.uuid; - const attendances = - !handleUser.isAttendancePublic || isSignedInUser - ? null - : (await UserAPI.getAttendancesForUserByUUID(token, user.uuid)).slice(0, 10); + let attendances = null; + if (!handleUser.isAttendancePublic || isSignedInUser) + attendances = (await UserAPI.getAttendancesForUserByUUID(token, user.uuid)).slice(0, 10); // render UserProfilePage return { From ecfe151ac531b365c560ecab84e88addd76d6e80 Mon Sep 17 00:00:00 2001 From: raymosun Date: Thu, 18 Jan 2024 21:28:17 -0800 Subject: [PATCH 22/22] Fix recent attendance bug --- src/pages/u/[handle].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/u/[handle].tsx b/src/pages/u/[handle].tsx index d6cd4390..2e7d800f 100644 --- a/src/pages/u/[handle].tsx +++ b/src/pages/u/[handle].tsx @@ -46,7 +46,7 @@ const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) const isSignedInUser = handleUser.uuid === user.uuid; let attendances = null; - if (!handleUser.isAttendancePublic || isSignedInUser) + if (!isSignedInUser && handleUser.isAttendancePublic) attendances = (await UserAPI.getAttendancesForUserByUUID(token, user.uuid)).slice(0, 10); // render UserProfilePage