-
{trim(name, 25)}
{rank}
diff --git a/src/components/profile/Preview/index.tsx b/src/components/profile/Preview/index.tsx
index a4a37b8c..34559c5e 100644
--- a/src/components/profile/Preview/index.tsx
+++ b/src/components/profile/Preview/index.tsx
@@ -1,13 +1,10 @@
-import { Typography } from '@/components/common';
+import { GifSafeImage, Typography } from '@/components/common';
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 { 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;
@@ -31,13 +28,12 @@ interface PreviewProps {
const Preview = ({ user, pfpCacheBust }: PreviewProps) => {
return (
-
{user.firstName} {user.lastName}
diff --git a/src/components/profile/Preview/style.module.scss b/src/components/profile/Preview/style.module.scss
index 4e26475c..0ad749a1 100644
--- a/src/components/profile/Preview/style.module.scss
+++ b/src/components/profile/Preview/style.module.scss
@@ -2,6 +2,7 @@
align-items: center;
display: flex;
flex-direction: column;
+
text-align: center;
.pfp {
@@ -33,6 +34,7 @@
.bio {
white-space: pre-wrap;
+ word-break: break-word;
}
.socials {
diff --git a/src/components/profile/UserHandleNotFound/index.tsx b/src/components/profile/UserHandleNotFound/index.tsx
new file mode 100644
index 00000000..4bc258d8
--- /dev/null
+++ b/src/components/profile/UserHandleNotFound/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 UserHandleNotFoundProps {
+ handle: string;
+}
+
+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
new file mode 100644
index 00000000..ad1d5a60
--- /dev/null
+++ b/src/components/profile/UserProfilePage/index.tsx
@@ -0,0 +1,172 @@
+import { Carousel, GifSafeImage, Typography } from '@/components/common';
+import { EventCard } from '@/components/events';
+import SocialMediaIcon from '@/components/profile/SocialMediaIcon';
+import { config, showToast } from '@/lib';
+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 LeaderboardIcon from '@/public/assets/icons/leaderboard-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 styles from './style.module.scss';
+
+export interface UserProfilePageProps {
+ handleUser: PublicProfile;
+ isSignedInUser: boolean;
+ signedInAttendances: PublicAttendance[];
+ attendances?: PublicAttendance[];
+}
+
+export const UserProfilePage = ({
+ handleUser,
+ attendances,
+ signedInAttendances,
+ isSignedInUser,
+}: UserProfilePageProps) => {
+ // animate the progress bar
+ const [progress, setProgress] = useState(0);
+ useEffect(() => setProgress(handleUser.points % 100), [handleUser.points]);
+
+ return (
+
+
+
+
+
+
+
+
+
{`${handleUser.firstName} ${handleUser.lastName}`}
+
+
+ {
+ copy(window.location.href);
+ showToast('Profile link copied!');
+ }}
+ >
+ @{handleUser.handle}
+
+
+
+
+
+
{getUserRank(handleUser.points)}
+
+
+ {handleUser.points.toLocaleString()} Leaderboard Points
+
+
+ {isSignedInUser && (
+
+ )}
+
+
+
+
+ {isSignedInUser ? 'My' : `${handleUser.firstName}'s`} Progress
+
+
+
Level {getLevel(handleUser.points)}
+
{handleUser.points % 100}/100
+
+
+
+ {isSignedInUser ? 'You need ' : `${handleUser.firstName} needs `}
+ {100 - (handleUser.points % 100)} more points to level up to
+
+ {getUserRank(handleUser.points + 100)}
+
+
+
+
+
+
+
About me
+
+
+
Class of {handleUser.graduationYear}
+
+
{handleUser.major}
+
+
+ {handleUser.userSocialMedia?.map(social => (
+
+
+
+ ))}
+
+
+
+ Bio
+
+ {handleUser.bio || Nothing here...}
+
+
+ {isSignedInUser && (
+
+ )}
+
+ {(attendances || isSignedInUser) && (
+
+
+ Recently Attended Events
+ {!handleUser.isAttendancePublic && (
+
+ (hidden for other users)
+
+ )}
+
+
+ {(isSignedInUser ? signedInAttendances : (attendances as PublicAttendance[])).map(
+ ({ event }) => (
+ uuid === event.uuid)}
+ />
+ )
+ )}
+
+
+ )}
+
+ );
+};
diff --git a/src/components/profile/UserProfilePage/style.module.scss b/src/components/profile/UserProfilePage/style.module.scss
new file mode 100644
index 00000000..7a1becda
--- /dev/null
+++ b/src/components/profile/UserProfilePage/style.module.scss
@@ -0,0 +1,230 @@
+@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 {
+ align-items: flex-start;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ @media (max-width: vars.$breakpoint-md) {
+ align-items: center;
+ flex-direction: column;
+
+ h1, div {
+ text-align: center;
+ }
+ }
+
+ .handle {
+ cursor: pointer;
+ }
+ }
+
+ .cardRank {
+ align-items: flex-end;
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ gap: 0.5rem;
+ justify-content: flex-end;
+
+ @media (max-width: vars.$breakpoint-md) {
+ align-items: center;
+ color: var(--theme-text-on-background-2);
+ flex-direction: column;
+ }
+
+ .rank {
+ color: var(--theme-text-on-background-1);
+ 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) {
+ margin-top: 0;
+ text-align: center;
+ }
+ }
+
+ .points {
+ color: var(--theme-text-on-background-1);
+ font-size: 1.125rem;
+ font-weight: 400;
+ line-height: 1.25;
+ text-align: end;
+
+ @media (max-width: vars.$breakpoint-md) {
+ text-align: center;
+ }
+
+ svg {
+ height: 1.125rem;
+ transform: translateY(calc((1.125rem * 1.25) - 1.125rem) * 0.5); // center the trophy icon
+ width: 1.125rem
+ }
+ }
+ }
+ }
+ }
+
+ .section {
+ border: 2px solid #808184;
+ border-radius: 0.75rem;
+ padding: 1rem 1.5rem;
+ }
+
+ .progressSection {
+ display: flex;
+ flex-direction: column;
+ gap: 0.9375rem;
+
+ .progressInfo {
+ display: flex;
+ flex-flow: row wrap;
+ justify-content: space-between;
+ }
+
+ .progressBar {
+ background-color: var(--theme-accent-line-2);
+ 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 {
+ align-items: center;
+ display: grid;
+ gap: 0.56rem;
+ grid-template-columns: auto 1fr;
+ margin: 0.81rem 0;
+
+ .icon {
+ height: 1.313rem;
+ width: 1.313rem;
+ }
+
+ }
+
+ .socialIcons {
+ display: flex;
+ gap: 0.25rem;
+
+ svg {
+ height: 2rem;
+ width: 2rem;
+ }
+ }
+
+ .bioSection {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+ }
+
+ .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/components/profile/UserProfilePage/style.module.scss.d.ts b/src/components/profile/UserProfilePage/style.module.scss.d.ts
new file mode 100644
index 00000000..b216de42
--- /dev/null
+++ b/src/components/profile/UserProfilePage/style.module.scss.d.ts
@@ -0,0 +1,30 @@
+export type Styles = {
+ aboutMeSection: string;
+ aboutSection: string;
+ banner: string;
+ bioSection: string;
+ card: string;
+ cardName: string;
+ cardRank: string;
+ cardWrapper: string;
+ editWrapper: string;
+ handle: string;
+ icon: string;
+ inner: string;
+ points: string;
+ profileCard: string;
+ profilePage: string;
+ profilePic: string;
+ progressBar: string;
+ progressInfo: string;
+ progressSection: string;
+ rank: string;
+ section: string;
+ socialIcons: string;
+};
+
+export type ClassNames = keyof Styles;
+
+declare const styles: Styles;
+
+export default styles;
diff --git a/src/components/profile/index.ts b/src/components/profile/index.ts
index 2304b886..af7d3385 100644
--- a/src/components/profile/index.ts
+++ b/src/components/profile/index.ts
@@ -2,3 +2,5 @@ export { EditBlock, EditField, SingleField } from './EditField';
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 a32531ca..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,23 +70,11 @@ 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}`;
-
- const response = await axios.get(requestUrl, {
- headers: {
- Authorization: `Bearer ${token}`,
- },
- });
-
- return response.data.attendances;
-};
-
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/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.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/constants/socialMediaTypes.ts b/src/lib/constants/socialMediaTypes.ts
index bc54883b..c2141cc2 100644
--- a/src/lib/constants/socialMediaTypes.ts
+++ b/src/lib/constants/socialMediaTypes.ts
@@ -1,12 +1,16 @@
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';
+import { ComponentType, SVGProps } from 'react';
interface SocialMediaInfo {
- icon: IconType;
+ icon: ComponentType>;
label: string;
domain?: string;
example: string;
@@ -20,41 +24,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/',
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..4d0dd14d 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';
import type { URL } from '@/lib/types';
import type {
CustomErrorBody,
@@ -7,6 +8,11 @@ import type {
ValidatorError,
} from '@/lib/types/apiResponses';
import NoImage from '@/public/assets/graphics/cat404.png';
+import {
+ type StaticImageData,
+ type StaticImport,
+ type StaticRequire,
+} from 'next/dist/shared/lib/get-img-props';
import { useEffect, useState } from 'react';
/**
@@ -72,19 +78,24 @@ 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,20 +110,28 @@ 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 rank based on how many points they have
+ * @param points
+ * @returns rank name
+ */
+export const getUserRank = (points: number): string => {
+ const index = Math.min(ranks.length, getLevel(points)) - 1;
+ return ranks[index] as string;
};
/**
- * 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
@@ -254,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/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/pages/events.tsx b/src/pages/events.tsx
index c6cc0f84..cf2853be 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';
@@ -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 EventAPI.getAttendancesForUser(authToken);
+ const [events, attendances] = await Promise.all([
+ EventAPI.getAllEvents(),
+ 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/leaderboard.tsx b/src/pages/leaderboard.tsx
index 592b6bb1..4b6c4e68 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
-
- >
- );
-};
+const UserProfilePage: NextPage = () => null;
export default UserProfilePage;
-const getServerSidePropsFunc: GetServerSideProps = async () => ({
- props: {},
-});
+const getServerSidePropsFunc: GetServerSideProps = async ({ req, res }) => {
+ const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
+ const user = await UserAPI.getCurrentUser(token);
+
+ return {
+ redirect: {
+ destination: `${config.userProfileRoute}${user.handle}`,
+ permanent: false,
+ },
+ };
+};
export const getServerSideProps = withAccessType(
getServerSidePropsFunc,
diff --git a/src/pages/profile/edit.tsx b/src/pages/profile/edit.tsx
index a35932ae..37f40d92 100644
--- a/src/pages/profile/edit.tsx
+++ b/src/pages/profile/edit.tsx
@@ -1,4 +1,4 @@
-import { Cropper } from '@/components/common';
+import { Cropper, GifSafeImage } from '@/components/common';
import {
EditBlock,
EditField,
@@ -9,19 +9,18 @@ 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';
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';
@@ -36,7 +35,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://
@@ -278,7 +278,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => {
onDragLeave={() => setPfpDrop(false)}
>
@@ -376,7 +375,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => {
-
+
About Me
@@ -385,7 +384,7 @@ const EditProfilePage = ({ user: initUser, authToken }: EditProfileProps) => {
{
}
name="major"
- options={data.majors}
+ options={majors}
element="select"
placeholder="Major"
error={errors.major}
diff --git a/src/pages/u/[handle].tsx b/src/pages/u/[handle].tsx
index 98b20c60..2e7d800f 100644
--- a/src/pages/u/[handle].tsx
+++ b/src/pages/u/[handle].tsx
@@ -1,30 +1,61 @@
+import {
+ UserHandleNotFound,
+ UserHandleNotFoundProps,
+ 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 type { PublicProfile } from '@/lib/types/apiResponses';
+import { setServerCookie } from '@/lib/services/CookieService';
import { CookieType } from '@/lib/types/enums';
import type { GetServerSideProps } from 'next/types';
-interface UserProfilePageProps {
- user: PublicProfile;
-}
+type UserHandlePageProps = UserHandleNotFoundProps | UserProfilePageProps;
-const UserProfilePage = ({ user }: UserProfilePageProps) => {
- return {JSON.stringify(user, null, 2)}
;
+const isUserHandleNotFound = (props: UserHandlePageProps): props is UserHandleNotFoundProps =>
+ 'handle' in props;
+
+const UserHandlePage = (props: UserHandlePageProps) => {
+ if (isUserHandleNotFound(props)) {
+ const { handle } = props;
+ return ;
+ }
+
+ return ;
};
-export default UserProfilePage;
+export default UserHandlePage;
const getServerSidePropsFunc: GetServerSideProps = async ({ params, req, res }) => {
const handle = params?.handle as string;
const token = CookieService.getServerCookie(CookieType.ACCESS_TOKEN, { req, res });
try {
- const user = await UserAPI.getUserByHandle(token, handle);
+ const [handleUser, user, signedInAttendances] = await Promise.all([
+ UserAPI.getUserByHandle(token, handle).catch(() => null),
+ UserAPI.getCurrentUser(token),
+ UserAPI.getAttendancesForCurrentUser(token),
+ ]);
+ setServerCookie(CookieType.USER, JSON.stringify(user), { req, res });
+
+ // render UserHandleNotFoundPage when user with handle is not retrieved
+ if (handleUser === null) return { props: { handle } };
+
+ const isSignedInUser = handleUser.uuid === user.uuid;
+
+ let attendances = null;
+ if (!isSignedInUser && handleUser.isAttendancePublic)
+ attendances = (await UserAPI.getAttendancesForUserByUUID(token, user.uuid)).slice(0, 10);
+
+ // render UserProfilePage
return {
props: {
- user,
+ handleUser,
+ isSignedInUser,
+ signedInAttendances,
+ attendances,
},
};
} catch (err: any) {
diff --git a/src/styles/pages/about.module.scss b/src/styles/pages/about.module.scss
index 4f6d4e78..6cc06b17 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: 24px;
+ width: 24px;
}
}
}
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;
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 {