diff --git a/frontend/package.json b/frontend/package.json index d301769d9..62b8ad6eb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,13 +17,19 @@ "prod": "serve -s build -p 3000" }, "dependencies": { + "airtable": "^0.12.2", + "bt": "^0.0.1", "classnames": "^2.5.1", - "iconoir-react": "^7.3.0", + "glider": "^0.1.0", + "iconoir-react": "^7.4.0", "moment": "^2.30.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-glider": "^4.0.2", + "react-multi-carousel": "^2.8.4", "react-router": "^6.21.3", - "react-router-dom": "^6.21.3" + "react-router-dom": "^6.21.3", + "swiper": "^11.0.7" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^4.3.0", @@ -36,8 +42,9 @@ "eslint": "^8.56.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", + "g-sheets-api": "^2.2.0", "prettier": "^3.2.4", - "sass": "^1.70.0", + "sass": "^1.71.1", "typescript": "^5.3.3", "vite": "^5.0.12" } diff --git a/frontend/public/images/christina_janet.jpeg b/frontend/public/images/christina_janet.jpeg new file mode 100644 index 000000000..465decb2b Binary files /dev/null and b/frontend/public/images/christina_janet.jpeg differ diff --git a/frontend/public/images/jemma.jpeg b/frontend/public/images/jemma.jpeg new file mode 100644 index 000000000..d55cf943d Binary files /dev/null and b/frontend/public/images/jemma.jpeg differ diff --git a/frontend/public/images/michaels.jpeg b/frontend/public/images/michaels.jpeg new file mode 100644 index 000000000..d8258463c Binary files /dev/null and b/frontend/public/images/michaels.jpeg differ diff --git a/frontend/public/images/retreat.png b/frontend/public/images/retreat.png new file mode 100644 index 000000000..f7fe2c0fd Binary files /dev/null and b/frontend/public/images/retreat.png differ diff --git a/frontend/public/images/retreat_silly.jpg b/frontend/public/images/retreat_silly.jpg new file mode 100644 index 000000000..161438920 Binary files /dev/null and b/frontend/public/images/retreat_silly.jpg differ diff --git a/frontend/public/images/will.jpeg b/frontend/public/images/will.jpeg new file mode 100644 index 000000000..5399822ec Binary files /dev/null and b/frontend/public/images/will.jpeg differ diff --git a/frontend/public/images/zoom.png b/frontend/public/images/zoom.png new file mode 100644 index 000000000..fd8b1e09d Binary files /dev/null and b/frontend/public/images/zoom.png differ diff --git a/frontend/src/app/About/About.module.scss b/frontend/src/app/About/About.module.scss new file mode 100644 index 000000000..31cd55e2f --- /dev/null +++ b/frontend/src/app/About/About.module.scss @@ -0,0 +1,315 @@ +//undefined stuff that was in main branch +$bt-navbar-height: 58px; +$bt-light-text: #8A8A8A; +$bt-base-text: #383838; +$bt-light-grey: #A0A0A0; + + + +.about { + padding-top: $bt-navbar-height; + + .aboutOurTeam { + text-align: center; + + margin-top: 48px; + margin-bottom: 48px; + + h1 { + margin-bottom: 8px; + font-weight: bold; + } + + p { + color: $bt-light-text; + line-height: 1.75; + width: 500px; + margin-bottom: 16px; + + @media (max-width: 768px) { + width: 300px; + } + } + } + + .group { + display: flex; + justify-content: center; + width: 100%; + position: relative; + overflow: hidden; + + .aboutCarousel { + /* Make the width of the flexbox equal the sum of the width of its items */ + display: inline-flex; + flex-direction: row; + justify-content: center; + + &.aboutCarouselSlideLeft { + transition: transform 1s ease; + transform: translateX(-20%); + } + + &.aboutCarouselSlideRight { + transition: transform 1s ease; + transform: translateX(20%); + } + + .aboutCarouselItem { + display: none; + transition: filter 1s ease, transform 1s ease; + + >img { + max-width: 100%; // Ensure image does not exceed the container width + height: auto; // Maintain aspect ratio + object-fit: cover; // Cover the container fully + } + + &.aboutCarouselActive { + display: block; + transform: translateX(0) scale(0.9); + filter: brightness(0.35); + } + + &.aboutCarouselActivePrev { + @extend .aboutCarouselActive; + order: 0; + } + + &.aboutCarouselActiveFirst { + @extend .aboutCarouselActive; + order: 1; + } + + &.aboutCarouselActiveSecond { + @extend .aboutCarouselActive; + order: 2; + transform: scale(1); + filter: brightness(1); + } + + &.aboutCarouselActiveThird { + @extend .aboutCarouselActive; + order: 3; + } + + &.aboutCarouselActiveNext { + @extend .aboutCarouselActive; + order: 4; + } + + &.focusIn { + transform: scale(1); + filter: brightness(1); + } + + &.focusOut { + transform: scale(0.9); + filter: brightness(0.35); + } + + >img { + border-radius: 3px; + width: 100%; + height: 100%; + object-fit: cover; + } + + @media (min-width: 1024px) { + height: 400px; + width: 711px; + } + + + @media (min-width: 768px) and (max-width: 1023) { + height: 300px; + width: 533px; + } + + @media (max-width: 767px) { + height: 200px; + width: 356px; + } + } + + // remove scroll bar + -ms-overflow-style: none; + /* Internet Explorer 10+ */ + scrollbar-width: none; + /* Firefox */ + } + + .aboutCarouselArrow { + position: absolute; + + top: calc(50% - 24px); + + display: grid; + place-items: center; + + height: 48px; + width: 48px; + + border: none; + + border-radius: 50%; + + background-color: rgba(white, 0.75); + + transition: background-color 0.15s ease-in-out; + + color: #383838; + + &:hover { + background-color: white; + } + } + + .aboutCarouselNext { + @extend .aboutCarouselArrow; + + right: calc(15% - 48px); + } + + .aboutCarouselPrev { + @extend .aboutCarouselArrow; + + left: calc(15% - 48px); + } + } + + + + // remove scroll bar + .group::-webkit-scrollbar { + /* Safari and Chrome */ + display: none; + } + + .values { + + margin-bottom: 80px; + + @media (min-width: 1024px) { + width: 900px; + } + + + @media (max-width: 767px) { + width: 400px; + } + + >h5 { + + margin-bottom: 40px; + text-align: center; + + font-weight: bold; + font-size: 24px; + + + @media (max-width: 768px) { + // Mobile + font-size: 20px; + } + } + + .valueCol { + margin-bottom: 20px; + + display: flex; + flex-direction: row; + + @media (max-width: 767px) { + flex-direction: column; + + >div:not(:last-child) { + margin-bottom: 20px; // For horizontal layout + // margin-bottom: 10px; // For vertical layout, comment out the margin-right + } + } + + .value { + height: 100%; + padding: 0 20px; + width: 600px; + + .valueContent { + height: 100%; + border-radius: 4px; + border: solid 2px #6d6a7238; + padding: 20px 10px; + + display: flex; + flex-direction: column; + align-items: center; + + >h6 { + font-size: 18px; + font-weight: bold; + margin: 20px 0 10px; + } + + >p { + text-align: center; + width: 95%; + line-height: 1.75; + color: #8a8a8a; + } + } + } + } + } + + // should move somewhere else??? + .releasesHeadingButton { + justify-content: center; + margin-top: 20px; + margin-bottom: 64px; + + input { + overflow: visible; + margin-right: 10px; + border: solid 1px #cfcfcf; + text-indent: 10px; + color: $bt-base-text; + border-radius: 4px; + width: 280px; + + &::placeholder { + color: $bt-light-grey; + } + } + } + + .joinPic { + width: 800px; + margin-bottom: 28px; + + @media (max-width: 768px) { + // Mobile + width: 95%; + } + } + + + .navLink { + background-color: #e1e1e16b; + } + + display: flex; + flex-direction: column; + align-items: center; + + .title { + font-weight: bold; + color: $bt-base-text; + font-size: 24px; + + @media (max-width: 768px) { + // Mobile + font-size: 30px; + } + + } +} \ No newline at end of file diff --git a/frontend/src/app/About/AboutCarousel.tsx b/frontend/src/app/About/AboutCarousel.tsx new file mode 100644 index 000000000..c5a88c2e8 --- /dev/null +++ b/frontend/src/app/About/AboutCarousel.tsx @@ -0,0 +1,138 @@ +import { useEffect, useRef, useState } from 'react'; +import { NavArrowLeft, NavArrowRight } from 'iconoir-react'; + +import michaels from '/images/michaels.jpeg'; +import retreat from '/images/retreat.png'; +import will from '/images/will.jpeg'; +import jemma from '/images/jemma.jpeg'; +import christina_janet from '/images/christina_janet.jpeg'; +import retreat_silly from '/images/retreat_silly.jpg'; +import zoom from '/images/zoom.png'; + + + + +const images = [ + { img: retreat_silly, alt: 'retreat silly' }, + { img: zoom, alt: 'zoom' }, + { img: retreat, alt: 'retreat' }, + { img: christina_janet, alt: 'christina_janet' }, + { img: michaels, alt: 'michaels' }, + { img: will, alt: 'will' }, + { img: jemma, alt: 'jemma' } +]; + + +import styles from './About.module.scss'; + +enum Sliding { + Still = 0, + Right = 1, + Left = 2 +} + + +const wrap = (val: number) => (val + images.length) % images.length; + +const AboutCarousel = () => { + const [shownImage, setShownImage] = useState(2); + const [queuedImage, setQueuedImage] = useState(2); + const [sliding, setSliding] = useState(Sliding.Still); + const intervalID = useRef(undefined); + + useEffect(() => { + const intervalHandler = () => { + if (sliding === Sliding.Still) { + setSliding(Sliding.Left); + setQueuedImage((prev) => wrap(prev + 1)); + } + }; + + intervalID.current = window.setInterval(intervalHandler, 5000); + + return () => { + clearInterval(intervalID.current); + }; + }, [sliding]); + + const changeImage = (slide: Sliding) => { + if (sliding === Sliding.Still) { + setSliding(slide); + const direction = slide === Sliding.Left ? 1 : -1; + setQueuedImage((prev) => wrap(prev + direction)); + } + }; + + const triggerSwap = () => { + setShownImage(queuedImage); + setSliding(Sliding.Still); + }; + + const getCarouselItemClass = (idx: number) => { + let classes = `${styles.aboutCarouselItem} `; + if (idx === wrap(shownImage - 2)) { + classes += `${styles.aboutCarouselActivePrev} `;; + } else if (idx === wrap(shownImage - 1)) { + classes += `${styles.aboutCarouselActiveFirst} `; + if (sliding === Sliding.Right) { + classes += `${styles.focusIn} `; + } + } else if (idx === shownImage) { + classes += `${styles.aboutCarouselActiveSecond} `; + if (sliding !== Sliding.Still) { + classes += `${styles.focusOut} `; + } + } else if (idx === wrap(shownImage + 1)) { + classes += `${styles.aboutCarouselActiveThird} `; + if (sliding === Sliding.Left) { + classes += `${styles.focusIn} `;; + } + } else if (idx === wrap(shownImage + 2)) { + classes += `${styles.aboutCarouselActiveNext} `;; + } + return classes.trim(); + }; + + const getCarouselClass = () => { + let classes = `${styles.aboutCarousel} `;; + if (sliding === Sliding.Left) { + classes += `${styles.aboutCarouselSlideLeft} `;; + } else if (sliding === Sliding.Right) { + classes += `${styles.aboutCarouselSlideRight} `;; + } + return classes.trim(); + }; + + return ( +
+
{ + if (e.target === e.currentTarget) triggerSwap(); + }} + > + {images.map((imgVal, index) => ( +
+ {imgVal.alt} +
+ ))} +
+ + +
+ ); +}; + +export default AboutCarousel; \ No newline at end of file diff --git a/frontend/src/app/About/Contributors.module.scss b/frontend/src/app/About/Contributors.module.scss new file mode 100644 index 000000000..57428decd --- /dev/null +++ b/frontend/src/app/About/Contributors.module.scss @@ -0,0 +1,101 @@ +$z-index-serious: 1; +$bt-grey-text: #535353; +$bt-light-text: #8A8A8A; +$bt-button-background: #8e9fc81c; + +.currentContributors, +.pastContributors { + display: flex; + flex-direction: column; + margin-bottom: 48px; + + h1 { + margin-bottom: 24px; + font-weight: bold; + } + + >div { + display: grid; + grid-template-columns: repeat(4, 150px); + justify-content: center; + flex-wrap: wrap; + gap: 25px; + + @media (max-width: 768px) { + grid-template-columns: repeat(2, 150px); + } + } +} + +.pastContributors { + margin-bottom: 16px; +} + +.contributorCard { + display: flex; + flex-direction: column; + align-items: flex-start; + margin-bottom: 25px; + + // use width/height to enforce aspect ratio i guess + .headshot { + + width: 150px; + height: 150px; + + margin-bottom: 10px; + + img { + width: 150px; + height: 150px; + + position: absolute; + object-fit: cover; + object-position: 50% 20%; + border-radius: 3px; + } + + .serious { + z-index: $z-index-serious; + transition: 0.2s; + + &:hover { + opacity: 0; + } + } + } + + .name { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + + width: 100%; + margin-bottom: 3px; + + p { + font-weight: 500; + color: $bt-grey-text; + } + + svg { + width: 22px; + height: 21px; + padding: 2px; + transition: 0.2s; + border-radius: 4px; + margin-bottom: 2px; + + &:hover { + background-color: $bt-button-background; + } + } + } + + .role { + font-size: 14px; + word-wrap: normal; + color: $bt-light-text; + } +} \ No newline at end of file diff --git a/frontend/src/app/About/Contributors.tsx b/frontend/src/app/About/Contributors.tsx new file mode 100644 index 000000000..e88d37f60 --- /dev/null +++ b/frontend/src/app/About/Contributors.tsx @@ -0,0 +1,96 @@ +import { Globe } from 'iconoir-react'; +import { contributorStructure } from './index'; +import styles from "./Contributors.module.scss" + +interface ContributorsProps { + currContributors: contributorStructure[]; + alumniContributors: contributorStructure[]; +} + + +const Contributors: React.FC = ({ currContributors, alumniContributors }) => { + /* console.log(currContributors) + console.log(alumniContributors) + console.log(currContributors[0]?.name) + console.log(currContributors[0]?.img.seriousBase64); + + */ + + console.log(currContributors) + const base64Root = 'data:image/jpeg;base64,' + + const alumniByGradYear: { [key: number]: contributorStructure[] } = {}; + + // Populate the data structure + alumniContributors.forEach((member) => { + const gradYear = member.gradYr; + if (gradYear in alumniByGradYear) { + alumniByGradYear[gradYear].push(member); + } else { + alumniByGradYear[gradYear] = [member]; + } + }); + + + + return ( + <> +
+

+ Current Contributors +

+
+ {currContributors.map(({ name, img, websiteURL, role }) => ( +
+
+ {name} + {name} +
+
+

{name}

+ {websiteURL ? ( + + + + ) : null} +
+
{role}
+
+ ))} +
+
+ +
+

+ Alumni +

+ {Object.entries(alumniByGradYear).map(([key, alumniList]) => ( +
+

+ {key.toString() === '00' ? 'Founders' : (key.toString() === '01' ? 'Founding Team' : `Class of ${key.toString()}`)} +

+
+ {alumniList.map((alumni) => ( // Iterate over each alumni in the list +
+
+

{alumni.name}

+ {alumni.websiteURL && ( + + + + )} +
+ {alumni.role &&
+ {key === '00' ? 'Co-Founder' : alumni.role} +
} +
+ ))} +
+
+ ))} +
+ + ); +}; + +export default Contributors; \ No newline at end of file diff --git a/frontend/src/app/About/index.tsx b/frontend/src/app/About/index.tsx index f8a4f997c..697c63ebc 100644 --- a/frontend/src/app/About/index.tsx +++ b/frontend/src/app/About/index.tsx @@ -1,3 +1,194 @@ + +import { Leaf, LightBulbOn, EmojiTalkingHappy } from 'iconoir-react'; +import Airtable from 'airtable'; +import { useState, useEffect } from 'react'; +import Contributors from './Contributors'; + + +//import CurrentContributors from '../components/About/CurrentContributors'; +//import PastContributors from '../components/About/PastContributors'; + +import AboutCarousel from './AboutCarousel'; + +import styles from "./About.module.scss" + +export interface contributorStructure { + name: string; + gradYr: number; + role: string; + img: { + seriousBase64: string, + sillyBase64: string | null, + }; + websiteURL: string; + isAlumni: boolean +}; + export default function About() { - return
; -} + + + + + const [currContributors, setCurrContributors] = useState([]); + const [alumniContributors, setAlumniContributors] = useState([]); + + useEffect(() => { + async function fetchData() { + await airtableDB(); + } + fetchData() + }, []) + + + async function convertURLToBase64(url: string) { + try { + // Fetch the image + const response = await fetch(url); + if (!response.ok) throw new Error('Network response was not ok.'); + + // Convert the image to a Blob + const blob = await response.blob(); + + // Create a FileReader to convert the Blob into a Base64 string + const reader = new FileReader(); + + // Return a promise that resolves with the Base64 string + return new Promise((resolve, reject) => { + reader.onloadend = () => { + // Extract the Base64 string (remove the Data URL part) + const result = reader.result as string; + var base64String + if (result) { + base64String = result.replace(/^data:.+;base64,/, ''); + } + else { + base64String = '' + } + resolve(base64String); + }; + + reader.onerror = error => { + reject(error); + }; + + // Read the Blob as a Data URL + reader.readAsDataURL(blob); + }); + } catch (error) { + console.error('Error converting image to Base64:', error); + throw error; + } + } + + //get data from airtable + function airtableDB() { + + Airtable.configure({ + endpointUrl: 'https://api.airtable.com', + apiKey: 'patx4Qcyifi4UHV3H.8b72c759aea1198b30510386c507581315bd5869f454cc93847c53d9863a5928' + }); + + var base = Airtable.base('appYnXDx7R3EomuMU'); + + base('General User Survey').select({ + view: "All Responses" + }).eachPage(async function page(records: ReadonlyArray, fetchNextPage) { // Mark this function as async + // Map over records to create an array of promises + const promises = records.map(async record => { + const seriousPicUrl = record.get("SeriousPicture")?.[0]?.url ?? null; + const sillyPicUrl = record.get("SillyPicture")?.[0]?.url ?? null; + console.log(sillyPicUrl) + const seriousBase64 = await convertURLToBase64(seriousPicUrl) as string; + const sillyBase64 = sillyPicUrl ? await convertURLToBase64(sillyPicUrl) as string : null; + const gradYear = record.get("GradYear") // Await the conversion + + return { + name: record.get("Name"), + gradYr: gradYear, + role: record.get("Role"), + img: { + seriousBase64: seriousBase64, + sillyBase64: sillyBase64, + }, // This will be a base64 string + websiteURL: record.get("Website"), + isAlumni: gradYear < (new Date().getFullYear()), + }; + }); + + // Wait for all promises to resolve + const teamMember = await Promise.all(promises); + // Push each team member into the currentTeam array + setCurrContributors(prevCurr => [...prevCurr, ...teamMember.filter(member => !member.isAlumni)]); + setAlumniContributors(prevAlumni => [...prevAlumni, ...teamMember.filter(member => member.isAlumni)]); + + fetchNextPage(); + }, function done(err) { + if (err) { console.error(err); return; } + }); + + } + + + //console.log(allMembers) + // console.log(currContributors) + //console.log(alumniContributors) + + return ( +
+
+

+ About Our Team +

+

+ We're a small group of student volunteers at UC Berkeley, dedicated to simplifying + the course discovery experience. We actively build, improve and maintain Berkeleytime. +

+ {/* */} +
+ + +
+
Our Values
+
+
+
+ +
Growth
+

+ You'll grow your technical skills as you tackle real challenging design and + engineering problems. +

+
+
+
+
+ +
Curiosity
+

+ We value team members that are curious about solving difficult problems and seek + out solutions independently. +

+
+
+ +
+
+ +
Passion
+

+ Genuine commitment and dedication are critical to moving the Berkeleytime product + forward. +

+
+
+
+
+ + {/* + */} +
+ ); +} \ No newline at end of file