diff --git a/.nvmrc b/.nvmrc index eb95d542..209e3ef4 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.5.0 \ No newline at end of file +20 diff --git a/apps/live/src/app/(landing)/EventSchedule/EventScheduleTabs.tsx b/apps/live/src/app/(landing)/EventSchedule/EventScheduleTabs.tsx index c7617d65..cee06912 100644 --- a/apps/live/src/app/(landing)/EventSchedule/EventScheduleTabs.tsx +++ b/apps/live/src/app/(landing)/EventSchedule/EventScheduleTabs.tsx @@ -195,7 +195,7 @@ const EventScheduleTabs = () => { const dateB = new Date(b.fields.start_time); return dateA.getTime() - dateB.getTime(); }) - .map((events) => { + .map((events, i) => { const { fields } = events; const { end_time, @@ -237,7 +237,7 @@ const EventScheduleTabs = () => { ); return ( ; -// avalibility: boolean; -// virtual: boolean; -// }; - -// // type MentorTableProps = { -// // data: AirtableData; -// // }; - -// // TODO: Add { data }: MentorTableProps back in -// const MentorsTable = () => { -// const [skillsFilter] = useState([]); -// const [availabilityFilter] = useState(false); -// const [virtualFilter] = useState(false); -// const [, setHasFilter] = useState(false); - -// // TODO: Add back in these original consts -// // const [skillsFilter, setSkillsFilter] = useState([]); -// // const [availabilityFilter, setAvailabilityFilter] = useState(false); -// // const [virtualFilter, setVirtualFilter] = useState(false); -// // const [hasFilter, setHasFilter] = useState(false); -// // const [dropdownOpen, setDropdownOpen] = useState(false); - -// // checking if current filter is default or not. if default it means dont have to do any filtering logic. -// useEffect(() => { -// setHasFilter( -// availabilityFilter || virtualFilter || skillsFilter.length !== 0, -// ); -// }, [virtualFilter, availabilityFilter, skillsFilter]); - -// // const { records } = data; - -// // const recordsExist = records != null && records != undefined; - -// // const uniqueSkills = new Set( -// // records -// // ?.map((record) => record.fields.Expertise) -// // .flat() -// // .map((skill) => skill.trim().toUpperCase()), -// // ); - -// // TODO in MentorsTable: Add this table back in once there is data in the airtable -// return ( -//
-// {/*
-//
-//
{ -// setDropdownOpen((prev) => !prev); -// }} -// > -// - -// -//
- -// {dropdownOpen && ( -//
-//
-// -// {[...uniqueSkills].map((skill) => ( -//
-// -//
-// ))} -//
-//
-// )} -//
-// - -// -//
- -//
-// {records -// .filter((filtered) => { -// if (!hasFilter) { -// return true; -// } - -// let virtual = !virtualFilter; -// let hasSkills = !skillsFilter.length; -// let isAvailable = !availabilityFilter; -// for (let i = 0; i < filtered.fields["Time Slots"].length; i++) { -// isAvailable = -// isTimeRange(filtered.fields["Time Slots"][i], date) || -// isAvailable; -// } - -// for (let i = 0; i < filtered.fields.Expertise.length; i++) { -// const cleanedSkill = filtered.fields.Expertise[i] -// .trim() -// .toUpperCase(); - -// hasSkills = skillsFilter.includes(cleanedSkill) || hasSkills; -// } - -// virtual = filtered.fields.IsVirtual === "True" || virtual; - -// return hasSkills && isAvailable && virtual; -// }) - -// .map((record, index) => ( -// -//
-// {record.id} -//
-// -// {record.fields.Name} -// -// -// ))} -//
*/} -//
-// ); -// }; - -// export default MentorsTable; +"use client"; + +import React, { useContext, useEffect, useMemo, useState } from "react"; +import Icon from "@repo/ui/Icons/MemberIcon"; +import clsx from "clsx"; +import isTimeRange from "@util/functions/isTimeRange"; +import useDevice from "@util/hooks/useDevice"; +import { AirtableData, MentorData } from "."; +import IconPopup from "@repo/ui/Icons/IconPopup"; +import { ModalContext } from "../providers"; + +type MentorTableProps = { + data: AirtableData; +}; + +const MentorsTable = ({ data }: MentorTableProps) => { + const { isDesktop, isTablet, isMobile } = useDevice(); + const records = useMemo(() => data?.records ?? [], [data]); + const [skillsFilter, setSkillsFilter] = useState([]); + const [availabilityFilter, setAvailabilityFilter] = useState(false); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [selectedMentor, setSelectedMentor] = useState(); + const { setModal } = useContext(ModalContext); + const gridStyles = clsx( + "flex flex-wrap justify-center items-center mx-auto gap-6", + isDesktop && "w-3/4", + isMobile && "grid grid-cols-2", + isTablet && "grid grid-cols-3", + ); + + const date = useMemo(() => new Date(), []); + + const uniqueSkills = useMemo(() => { + const skills = new Set(); + records.forEach((r) => { + r.fields.Expertise?.forEach((s) => skills.add(s.trim())); + }); + return Array.from(skills).sort(); + }, [records]); + + const hasFilter = availabilityFilter || skillsFilter.length !== 0; + + const toggleSkill = (skill: string) => { + setSkillsFilter((prev) => { + if (prev.includes(skill)) return prev.filter((s) => s !== skill); + return [...prev, skill]; + }); + }; + + const filtered = useMemo(() => { + if (!records.length) return []; + return records.filter((rec) => { + if (!hasFilter) return true; + + const expertise = new Set( + rec.fields.Expertise?.map((s) => s.trim()) ?? [], + ); + const hasSkills = + skillsFilter.length === 0 || + skillsFilter.every((skill) => expertise.has(skill)); + + let availableOk = true; + if (availabilityFilter) { + availableOk = false; + const slots = rec.fields["Time Slots"] || []; + for (let i = 0; i < slots.length; i++) { + if (isTimeRange(slots[i], date)) { + availableOk = true; + break; + } + } + } + + return hasSkills && availableOk; + }); + }, [records, skillsFilter, availabilityFilter, hasFilter, date]); + + useEffect(() => { + if (selectedMentor) { + setModal( + { + setSelectedMentor(null); + setModal(null); + }} + />, + ); + } + }, [selectedMentor]); + + if (!records.length) { + return
No mentors available right now.
; + } + + return ( +
+
+
+ + + {dropdownOpen && ( +
+
+ + {uniqueSkills.map((skill) => ( + + ))} +
+
+ )} +
+ + +
+ +
+ {(hasFilter ? filtered : records).map((record) => { + const slots = record.fields["Time Slots"] || []; + let isAvailable = false; + for (let i = 0; i < slots.length; i++) { + if (isTimeRange(slots[i], date)) { + isAvailable = true; + break; + } + } + + const imageUrl = + record.fields.Image?.[0]?.url || "/headshots/placeholder.png"; + + return ( +
+ setSelectedMentor(record)} + /> +
+ ); + })} +
+
+ ); +}; + +export default MentorsTable; diff --git a/apps/live/src/app/(landing)/Mentors/index.tsx b/apps/live/src/app/(landing)/Mentors/index.tsx index 0b8d6efd..1fa8bc2c 100644 --- a/apps/live/src/app/(landing)/Mentors/index.tsx +++ b/apps/live/src/app/(landing)/Mentors/index.tsx @@ -3,7 +3,8 @@ import React, { useEffect, useRef, useState } from "react"; import Image from "next/image"; import Section from "@repo/ui/Section"; -// import MentorsTable from "./MentorsTable"; +import RibbonTitle from "@repo/ui/RibbonTitle"; +import MentorsTable from "./MentorsTable"; import useContentHeight from "@util/hooks/useContentHeight"; import useWindowSize from "@util/hooks/useWindowSize"; @@ -14,24 +15,20 @@ export type AirtableImage = { url: string; }; -export type AirtableRecord = { +export type MentorData = { id: string; createdTime: string; fields: { - Availability: string; - Email: string; "Time Slots": Array; Name: string; - Image: Array<{ url: string }>; + Image: Array; Expertise: Array; - Certification: string; - LinkedIn: string; - IsVirtual: "True" | "False"; + discord: string; }; }; export type AirtableData = { - records: AirtableRecord[]; + records: MentorData[]; }; const MentorSection = () => { @@ -42,7 +39,7 @@ const MentorSection = () => { useEffect(() => { async function fetchData() { - const res = await fetch("/api/mentors"); + const res = await fetch(`/api/mentors`); const jsonData: AirtableData = await res.json(); setData(jsonData); } @@ -66,18 +63,23 @@ const MentorSection = () => { const MentorSectionContent = React.forwardRef((_, ref) => { return ( -
-

- Our Mentors -

-

- Need expert advice? Our mentors are here to help! Filter by shift, - virtual status, expertise, or company to find the right support. - Connect on our hackathon platform and get insights to take your - project to the next level! -

- {/* TODO: Add MentorsTable back in when there is data */} - {/* */} +
+
+ +
+
+

+ Need expert advice? +

+

+ Our mentors are here to help! Filter by shift, virtual status, + expertise, or company to find the right support. Connect on our + hackathon platform and get insights to take your project to the next + level! +

+
+ {/* Mentor listing (renders when Airtable data available) */} +
); }); diff --git a/apps/live/src/app/(landing)/OurTeam/OurTeamGrid.tsx b/apps/live/src/app/(landing)/OurTeam/OurTeamGrid.tsx index 4149f33b..e7ec21cd 100644 --- a/apps/live/src/app/(landing)/OurTeam/OurTeamGrid.tsx +++ b/apps/live/src/app/(landing)/OurTeam/OurTeamGrid.tsx @@ -217,9 +217,9 @@ const TeamTable = () => { return (
- {Object.entries(teams).map(([teamName, team]) => ( + {Object.entries(teams).map(([teamName]) => (
- {teams[currTeam].map((member, index) => ( - - - + {teams[currTeam].map(({ url, src, name }, index) => ( + ))}
diff --git a/apps/live/src/app/(landing)/providers.tsx b/apps/live/src/app/(landing)/providers.tsx index 118beb8b..aa50a9b8 100644 --- a/apps/live/src/app/(landing)/providers.tsx +++ b/apps/live/src/app/(landing)/providers.tsx @@ -1,16 +1,50 @@ "use client"; import useIsMobile from "@repo/util/hooks/useIsMobile"; -import React, { createContext } from "react"; +import React, { createContext, ReactElement, useState } from "react"; + +export type ModalContextProps = { + modal: ReactElement | null; + setModal: (component: ReactElement | null) => void; +}; export const MobileContext = createContext({ isMobile: false }); +export const ModalContext = createContext({ + modal: null, + setModal: () => {}, +}); export function Providers({ children }: { children: React.ReactNode }) { const isMobile = useIsMobile(); + const [modalComponent, setModalComponent] = useState( + null, + ); + console.log(modalComponent); return ( - {children} + +
+ {!!modalComponent && ( +
+
{ + setModalComponent(null); + console.log("blurred"); + }} + > + {modalComponent} +
+
+ )} + + {children} +
+
); } diff --git a/apps/live/src/app/api/mentors/route.ts b/apps/live/src/app/api/mentors/route.ts index 2c575c41..52e7e5ec 100644 --- a/apps/live/src/app/api/mentors/route.ts +++ b/apps/live/src/app/api/mentors/route.ts @@ -1,13 +1,193 @@ -import { NextResponse, NextRequest } from "next/server"; +import { NextResponse } from "next/server"; const BASE_URL = "https://api.airtable.com/v0"; -const TABLE_NAME = "2025mentors"; +const TABLE_NAME = "mentors"; + +const USE_SAMPLE_DATA = false; + +const test = { + records: [ + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Web"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["00:00-23:59"], + IsVirtual: "True", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["ML", "Data"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["08:00-20:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Mobile"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["12:00-12:30"], + IsVirtual: "True", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Design"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["09:30-10:30"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["DevOps"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["00:00-23:59"], + IsVirtual: "True", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Security"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["22:00-02:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Product"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["07:00-09:00"], + IsVirtual: "True", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Web", "Design"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["00:00-23:59"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["AI", "ML"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["16:00-18:00"], + IsVirtual: "True", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Cloud"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["10:00-12:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["VR"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["00:00-23:59"], + IsVirtual: "True", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Frontend", "Accessibility"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["13:00-15:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Frontend", "Accessibility"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["13:00-15:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Frontend", "Accessibility"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["13:00-15:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Frontend", "Accessibility"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["13:00-15:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Frontend", "Accessibility"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["13:00-15:00"], + IsVirtual: "False", + }, + }, + { + fields: { + Name: "Emma", + Image: [{ url: "/headshots/directors/Emma.jpg" }], + Expertise: ["Frontend", "Accessibility"], + LinkedIn: "https://www.linkedin.com/in/emma-von/", + "Time Slots": ["13:00-15:00"], + IsVirtual: "False", + }, + }, + ], +}; + +export async function GET() { + if (USE_SAMPLE_DATA) { + return NextResponse.json(test); + } -export async function GET(req: NextRequest) { - const { searchParams } = new URL(req.url); - const isDev = searchParams.get("isDev") === "true"; const MENTOR_BASE_ID = process.env.MENTOR_BASE_ID; - const airtableUrl = `${BASE_URL}/${MENTOR_BASE_ID}/${TABLE_NAME}${isDev ? "?isDev=true" : ""}`; + const airtableUrl = `${BASE_URL}/${MENTOR_BASE_ID}/${TABLE_NAME}`; + try { const response = await fetch(`${airtableUrl}`, { headers: { @@ -17,12 +197,17 @@ export async function GET(req: NextRequest) { }); if (!response.ok) { - throw new Error("API request failed"); + const body = await response.json().catch(() => null); + return NextResponse.json( + { error: body?.error?.message || "Airtable API request failed" }, + { status: response.status }, + ); } const data = await response.json(); return NextResponse.json(data); } catch (err) { + console.error("Mentors API error:", err); return NextResponse.json( { error: `Request to get airtable data failed ${err}` }, { status: 500 }, diff --git a/packages/ui/src/Icons/IconPopup.tsx b/packages/ui/src/Icons/IconPopup.tsx index 044dcc4e..23bbc568 100644 --- a/packages/ui/src/Icons/IconPopup.tsx +++ b/packages/ui/src/Icons/IconPopup.tsx @@ -7,20 +7,41 @@ import CloseIcon from "./CloseIcon"; import useDevice from "@repo/util/hooks/useDevice"; import React from "react"; -const IconPopup = () => { +type IconPopupProps = { + iconSrc: string; + iconAltText: string; + iconTitle: string; + discord: string; + expertise: string[]; + onClose: () => void; +}; + +const IconPopup = ({ + iconSrc, + iconAltText, + iconTitle, + expertise, + onClose, +}: IconPopupProps) => { const { isDesktop } = useDevice(); return ( -
+
-
+
@@ -37,8 +58,8 @@ const IconPopup = () => {
Emma Voneulow {
-

Emma Voneulow

+

{iconTitle}

-

emmavonbeulow

+

{iconAltText}

@@ -62,7 +83,7 @@ const IconPopup = () => {

Expertise

-

React, Typescript, Next.js, Tailwind CSS, Python

+

{expertise.join(", ")}


diff --git a/packages/ui/src/Icons/MemberIcon.tsx b/packages/ui/src/Icons/MemberIcon.tsx index 45e3843e..ceb97576 100644 --- a/packages/ui/src/Icons/MemberIcon.tsx +++ b/packages/ui/src/Icons/MemberIcon.tsx @@ -12,6 +12,8 @@ type IconProps = { isLive: boolean; isActive: boolean; textColor?: string; + showLinkedInIcon?: boolean; + onClick?: () => void; }; const Icon: React.FC = ({ @@ -21,9 +23,11 @@ const Icon: React.FC = ({ isLive = false, isActive = false, textColor = "charcoalFog", + showLinkedInIcon = false, + onClick, }) => { return ( -
+
= ({ opacity-0 group-hover:opacity-100 transition-opacity duration-300F" > - {isLive ? : } + {isLive && !showLinkedInIcon ? : }
diff --git a/packages/util/src/functions/isTimeRange.ts b/packages/util/src/functions/isTimeRange.ts index 76b3371c..2b1b9cd0 100644 --- a/packages/util/src/functions/isTimeRange.ts +++ b/packages/util/src/functions/isTimeRange.ts @@ -17,4 +17,27 @@ export default function isTimeRange(range: string, currentTime: Date) { return currentTime >= startTime && currentTime <= endTime; } + + const simpleMatch = range.match( + /^(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})$/, + ); + if (simpleMatch) { + const startHour = Number(simpleMatch[1]); + const startMinute = Number(simpleMatch[2]); + const endHour = Number(simpleMatch[3]); + const endMinute = Number(simpleMatch[4]); + + const start = new Date(currentTime); + start.setHours(startHour, startMinute, 0, 0); + const end = new Date(currentTime); + end.setHours(endHour, endMinute, 0, 0); + + if (end < start) { + return currentTime >= start || currentTime <= end; + } + + return currentTime >= start && currentTime <= end; + } + + return false; }