diff --git a/frontend/app/api/execs/route.js b/frontend/app/api/execs/route.js new file mode 100644 index 0000000..2e6184c --- /dev/null +++ b/frontend/app/api/execs/route.js @@ -0,0 +1,48 @@ +import { + getSupabaseCredentials, + hasMissingAnonCredentials, + requestSupabaseEdgeFunction, +} from "../utils/supabase"; + +export async function GET() { + const credentials = getSupabaseCredentials(); + + if (hasMissingAnonCredentials(credentials)) { + return Response.json( + { + error: + "Server misconfiguration: SUPABASE_URL and/or SUPABASE_ANON_KEY are not set.", + }, + { status: 500 }, + ); + } + + try { + const { response, body } = await requestSupabaseEdgeFunction( + credentials.url, + "exec", + credentials.anonKey, + ); + + if (!response.ok) { + return Response.json( + { + error: + body?.error || + body?.message || + `Upstream error from Supabase: ${response.status} ${response.statusText}`, + }, + { status: response.status }, + ); + } + + return Response.json(body); + } catch (error) { + console.error("Failed to fetch executive data:", error); + + return Response.json( + { error: "Unable to retrieve executive data." }, + { status: 500 }, + ); + } +} \ No newline at end of file diff --git a/frontend/components/ExecutiveSection.tsx b/frontend/components/ExecutiveSection.tsx index 76e1724..49e704d 100644 --- a/frontend/components/ExecutiveSection.tsx +++ b/frontend/components/ExecutiveSection.tsx @@ -1,246 +1,198 @@ +"use client"; + +import { useEffect, useState } from "react"; import FlipCard from "./FlipCard"; import SectionHeader from "./SectionHeader"; +import ErrorState from "./ErrorState"; -const execTeamData = [ - { - title: "Presidents", - members: [ - { - name: "Danielle Santos", - position: "Co-President", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Josephine Tjhia", - position: "Co-President", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - } - ] - }, - { - title: "Marketing", - members: [ - { - name: "Vicky Zhong", - position: "VP Marketing", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Krisha Patel", - position: "Director of Graphics", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Maissara Said Bakar", - position: "Director of Campus", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Conrad Mo", - position: "Director of Socials", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Fareeha Noor", - position: "Graphic Designer", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Kaitlyn Qian", - position: "Graphic Designer", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - } - ] - }, - { - title: "Finance", - members: [ - { - name: "Rayyan Anam", - position: "VP Finance", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - } - ] - }, - { - title: "Events", - members: [ - { - name: "Jennifer Huang", - position: "VP Events", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Shangavi Muhunthan", - position: "Event Coordinator", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Shubham Garg", - position: "Event Coordinator", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Sylvia Xu", - position: "Event Coordinator", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - } - ] - }, - { - title: "Corporations", - members: [ - { - name: "Maanya Gupta", - position: "Associate", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Kehu Li", - position: "Associate", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Shubhra Rashmi", - position: "Associate", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - } - ] - }, - { - title: "Mentorship", - members: [ - { - name: "Arushi Bhatt", - position: "VP Mentorship", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Kayleigh Doopan", - position: "Associate", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - } - ] - }, - { - title: "First Years", - members: [ - { - name: "Rose Gu", - position: "First Year Rep", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Ire Oluyemi", - position: "First Year Rep", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - }, - { - name: "Thanusa Paskaran", - position: "First Year Rep", - description: "This person is super important for WiCSM. This person is super important for WiCSM.", - imageUrl: "/IMG_1710.jpg", - } - ] - } +async function getExecs() { + const response = await fetch("/api/execs"); + const data = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error(data.error || "Failed to fetch executive data"); + } + + if (!Array.isArray(data.execs)) { + throw new Error("Executive data was returned in an invalid format"); + } + + return data.execs; +} + +const SECTION_CONFIG = [ + { title: "Presidents", orderIndexes: [[1, 2]] }, + { title: "Marketing", orderIndexes: [[3, 11]] }, + { title: "Events", orderIndexes: [[12, 15]] }, + { title: "Corporations", orderIndexes: [[16, 18]] }, + { title: "Mentorship", orderIndexes: [[19, 20]] }, + { title: "Finance", orderIndexes: [[21, 21]] }, ]; -export default function ExecutiveSection() { +function orderIndexInRanges(orderIndex, ranges) { + return ranges.some( + ([start, end]) => orderIndex >= start && orderIndex <= end, + ); +} + +function groupExecs(execs) { + const assignedIds = new Set(); + + const sections = SECTION_CONFIG.map(({ title, orderIndexes }) => { + const members = execs.filter((member) => { + if (orderIndexInRanges(member.order_index, orderIndexes)) { + assignedIds.add(member.id); + return true; + } + + return false; + }); + + return { + title, + members, + }; + }).filter((section) => section.members.length > 0); + + const remainingMembers = execs.filter( + (member) => !assignedIds.has(member.id), + ); + + if (remainingMembers.length > 0) { + sections.push({ + title: "Executive Team", + members: remainingMembers, + }); + } + + return sections; +} + +function splitByRole(members) { + const vicePresidents = members.filter((member) => { + const role = member.role ?? ""; + return ( -
-
- - Executive Team - - -

- Meet the amazing people at WiCSM! -

- - {execTeamData.map((team) => { - const vpMembers = team.members.filter(m => m.position.includes("Vice President") || m.position.includes("Co-President") || m.position.includes("VP")); - const directorMembers = team.members.filter(m => m.position.includes("Director")); - const otherMembers = team.members.filter(m => - !m.position.includes("Vice President") && - !m.position.includes("Co-President") && - !m.position.includes("VP") && - !m.position.includes("Director") - ); - - return ( -
-

- {team.title} -

- - {/* VPs/Co-Prez row */} - {vpMembers.length > 0 && ( -
- {vpMembers.map((member) => ( - - ))} -
- )} - - {/* Directors row */} - {directorMembers.length > 0 && ( -
- {directorMembers.map((member) => ( - - ))} -
- )} - - {/* Other Members row */} - {otherMembers.length > 0 && ( -
- {otherMembers.map((member) => ( - - ))} -
- )} -
- ); - })} -
-
+ role.includes("Vice President") || + role.includes("Co-President") || + role.includes("VP") ); + }); + + const directors = members.filter((member) => { + const role = member.role ?? ""; + return role.includes("Director"); + }); + + const otherMembers = members.filter((member) => { + const role = member.role ?? ""; + + return ( + !role.includes("Vice President") && + !role.includes("Co-President") && + !role.includes("VP") && + !role.includes("Director") + ); + }); + + return { + vicePresidents, + directors, + otherMembers, + }; +} + +function MemberRow({ members }) { + if (members.length === 0) { + return null; + } + + return ( +
+ {members.map((member) => ( + + ))} +
+ ); +} + +export default function ExecutiveSection() { + const [execTeamData, setExecTeamData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + + useEffect(() => { + async function loadExecs() { + try { + setLoading(true); + setError(""); + + const execs = await getExecs(); + setExecTeamData(groupExecs(execs)); + } catch (err) { + setError( + err instanceof Error + ? err.message + : "Unable to load executive data", + ); + } finally { + setLoading(false); + } + } + + loadExecs(); + }, []); + + return ( +
+
+ Executive Team + +

+ Meet the amazing people at WiCSM! +

+ + {loading && ( +

+ Loading team... +

+ )} + + {!loading && error && ( + + )} + + {!loading && + !error && + execTeamData.map((team) => { + const { + vicePresidents, + directors, + otherMembers, + } = splitByRole(team.members); + + return ( +
+

+ {team.title} +

+ + + + +
+ ); + })} +
+
+ ); } \ No newline at end of file diff --git a/frontend/components/FlipCard.tsx b/frontend/components/FlipCard.tsx index aa61cb2..2523c58 100644 --- a/frontend/components/FlipCard.tsx +++ b/frontend/components/FlipCard.tsx @@ -83,4 +83,4 @@ export default function FlipCard({ name, position, description, imageUrl }) { ); -} +} \ No newline at end of file diff --git a/frontend/components/Footer.tsx b/frontend/components/Footer.tsx index 41a2f85..a0b0bf3 100644 --- a/frontend/components/Footer.tsx +++ b/frontend/components/Footer.tsx @@ -29,7 +29,7 @@ export default function Footer() { {/* Quick Links */}
Home - About + About Events Contact
diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index bc05ee2..6e0266a 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -6,7 +6,7 @@ import { useState } from "react"; const navItems = [ { href: "/", label: "HOME" }, - { href: "/about", label: "ABOUT" }, + { href: "/aboutus", label: "ABOUT" }, { href: "/events", label: "EVENTS" }, { href: "/contact", label: "CONTACT" }, ]; diff --git a/frontend/components/PersonPlaceholder.jsx b/frontend/components/PersonPlaceholder.jsx new file mode 100644 index 0000000..7e8cec1 --- /dev/null +++ b/frontend/components/PersonPlaceholder.jsx @@ -0,0 +1,21 @@ +export default function PersonPlaceholder() { + return ( +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..2d8c49b 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,13 @@ -import type { NextConfig } from "next"; - -const nextConfig: NextConfig = { - /* config options here */ +const nextConfig = { + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "uhugvwdetrwvhhrsyvoz.supabase.co", + pathname: "/storage/v1/object/public/exec-images/**", + }, + ], + }, }; -export default nextConfig; +export default nextConfig; \ No newline at end of file