diff --git a/docs/posters/labconnect_F25_poster.pdf b/docs/posters/labconnect_F25_poster.pdf new file mode 100644 index 00000000..1b65ecc3 Binary files /dev/null and b/docs/posters/labconnect_F25_poster.pdf differ diff --git a/docs/posters/labonnect_S25_poster (1).pdf b/docs/posters/labonnect_S25_poster (1).pdf new file mode 100644 index 00000000..bd5cba1c Binary files /dev/null and b/docs/posters/labonnect_S25_poster (1).pdf differ diff --git a/src/App.tsx b/src/App.tsx index 1c3b5fc2..6076bf15 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -36,7 +36,6 @@ function App() { } /> } /> } /> - } /> } diff --git a/src/context/OpportunityContext.tsx b/src/context/OpportunityContext.tsx new file mode 100644 index 00000000..43c4ffe2 --- /dev/null +++ b/src/context/OpportunityContext.tsx @@ -0,0 +1,171 @@ +// src/context/OpportunityContext.tsx +import React, { createContext, useReducer, useCallback } from "react"; +import { OpportunityAction } from "../types/opportunityaction"; +import { Opportunity } from "../types/opportunity"; +import { Filters } from "../types/filters"; + +const currYr = new Date().getFullYear(); + +interface OpportunityState { + filters: { + activeFilters: string[]; + filterMap: Filters; + }; + query: string; + activeId: string; + opportunities: Opportunity[]; +} + +const initialState: OpportunityState = { + filters: { + activeFilters: ["2025"], + filterMap: { years: [2025], credits: [], hourlyPay: 0, majors: [] }, + }, + query: "", + activeId: "", + opportunities: [], +}; + +export interface OpportunityContextType { + activeFilters: OpportunityState["filters"]["activeFilters"]; + filterMap: OpportunityState["filters"]["filterMap"]; + opportunities: OpportunityState["opportunities"]; + resetFilters: () => void; + removeFilter: (name: string) => void; + setFilters: (activeFilters: string[], filterMap: Filters) => void; + setQuery: (query: string) => void; + setOpportunities: (opportunities: Opportunity[]) => void; +} + +// Stable default values for HMR-safe context +const noop = () => {}; + +export const OpportunityContext = createContext({ + activeFilters: initialState.filters.activeFilters, + filterMap: initialState.filters.filterMap, + opportunities: initialState.opportunities, + resetFilters: noop, + removeFilter: noop, + setFilters: noop, + setQuery: noop, + setOpportunities: noop, +}); + +export const OpportunityProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [opportunityState, dispatch] = useReducer( + (state: OpportunityState, action: OpportunityAction) => { + switch (action.type) { + case "SET_QUERY": + return { ...state, query: action.query }; + + case "SET_FILTERS": + if (!action.activeFilters || !action.filterMap) return state; + return { + ...state, + filters: { + activeFilters: action.activeFilters, + filterMap: action.filterMap, + }, + }; + + case "RESET_FILTERS": + return { + ...state, + filters: { + activeFilters: [currYr.toString()], + filterMap: { + years: [currYr], + credits: [], + hourlyPay: 0, + majors: [], + }, + }, + }; + + case "REMOVE_FILTER": + if (!action.filter) return state; + const newActiveFilters = state.filters.activeFilters.filter( + (filter) => filter !== action.filter + ); + const newFilterMap = { + ...state.filters.filterMap, + years: state.filters.filterMap.years.filter( + (year) => year !== parseInt(action.filter) + ), + credits: action.filter.includes("Credit") + ? [] + : state.filters.filterMap.credits, + majors: state.filters.filterMap.majors.filter( + (major) => major !== action.filter + ), + hourlyPay: action.filter.includes("$") + ? 0 + : state.filters.filterMap.hourlyPay, + }; + return { + ...state, + filters: { + activeFilters: newActiveFilters, + filterMap: newFilterMap, + }, + }; + + case "SET_OPPORTUNITIES": + if (!action.opportunities) return state; + return { ...state, opportunities: action.opportunities }; + + default: + return state; + } + }, + initialState + ); + + const resetFilters = useCallback( + () => dispatch({ type: "RESET_FILTERS" }), + [] + ); + + const removeFilter = useCallback( + (name: string) => dispatch({ type: "REMOVE_FILTER", filter: name }), + [] + ); + + const setFilters = useCallback( + (activeFilters: string[], filterMap: Filters) => + dispatch({ type: "SET_FILTERS", activeFilters, filterMap }), + [] + ); + + const setQuery = useCallback( + (query: string) => dispatch({ type: "SET_QUERY", query }), + [] + ); + + const setOpportunities = useCallback( + (opportunities: Opportunity[]) => + dispatch({ type: "SET_OPPORTUNITIES", opportunities }), + [] + ); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/context/useOpportunity.tsx b/src/context/useOpportunity.tsx new file mode 100644 index 00000000..9d1c5e33 --- /dev/null +++ b/src/context/useOpportunity.tsx @@ -0,0 +1,6 @@ +import { useContext } from "react"; +import { OpportunityContext } from "./OpportunityContext"; + +export const useOpportunity = () => { + return useContext(OpportunityContext); +}; \ No newline at end of file diff --git a/src/individuals/pages/Saved.tsx b/src/individuals/pages/Saved.tsx index 4a5ecb69..00634b08 100644 --- a/src/individuals/pages/Saved.tsx +++ b/src/individuals/pages/Saved.tsx @@ -33,94 +33,97 @@ export default function SavedPage() { } catch { console.log("Error fetching saved"); } + console.log(saved) } useEffect(() => { fetchSaved(); }, []); - return ( -
-

- Saved Opportunities -

+
+
{!saved && "Loading..."} {saved && ( - - - - - - - - - - - - - - {saved.map((opportunity) => ( - - - - - - - - - diff --git a/src/opportunities/components/PopUpMenu.tsx b/src/opportunities/components/PopUpMenu.tsx index 7789a33e..1fe1adf5 100644 --- a/src/opportunities/components/PopUpMenu.tsx +++ b/src/opportunities/components/PopUpMenu.tsx @@ -2,13 +2,11 @@ import React, { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import CheckBox from "../../shared/components/Checkbox.tsx"; import Input from "../../staff/components/Input.jsx"; -import { Filters } from "../../types/opportunities.ts"; +import { Filters } from "../../types/filters.ts"; +import { useOpportunity } from "../../context/useOpportunity.tsx"; interface PopUpMenuProps { - setFunction: () => void; - reset: () => void; - filters: Filters; - setFilters: (activeFilters: string[], filterMap: Filters) => void; + setOpen: () => void; } interface Major { @@ -16,7 +14,8 @@ interface Major { name: string; } -export default function PopUpMenu({ setFunction, reset, filters, setFilters }: PopUpMenuProps) { +export default function PopUpMenu({ setOpen }: PopUpMenuProps) { + const { filterMap, setFilters, resetFilters } = useOpportunity(); const [majors, setMajors] = useState(); const [validYears, setValidYears] = useState([]); @@ -61,7 +60,7 @@ export default function PopUpMenu({ setFunction, reset, filters, setFilters }: P defaultValues: { years: [], credits: [], - hourlyPay: filters.hourlyPay ?? 0, + hourlyPay: filterMap.hourlyPay ?? 0, majors: [] }, }); @@ -105,7 +104,7 @@ export default function PopUpMenu({ setFunction, reset, filters, setFilters }: P ...majors ]; setFilters(activeFilters, newFilterMap); - setFunction() + setOpen() }; return ( @@ -134,7 +133,7 @@ export default function PopUpMenu({ setFunction, reset, filters, setFilters }: P formHook={{ ...register(filter[2], {}) }} name={filter[2]} type="checkbox" - filters={filters} + filters={filterMap} /> ))} @@ -176,7 +175,7 @@ export default function PopUpMenu({ setFunction, reset, filters, setFilters }: P key={index} value={major.code} className="py-2 px-3 hover:bg-blue-100" - selected={filters.majors.includes(major.code)} + selected={filterMap.majors.includes(major.code)} > {major.name} @@ -188,10 +187,10 @@ export default function PopUpMenu({ setFunction, reset, filters, setFilters }: P
- +
- +
diff --git a/src/opportunities/components/Posts.tsx b/src/opportunities/components/Posts.tsx index a13ab5fa..90bf20a4 100644 --- a/src/opportunities/components/Posts.tsx +++ b/src/opportunities/components/Posts.tsx @@ -1,125 +1,21 @@ +import { useCallback, useEffect, useState } from "react"; import FiltersField from "./FiltersField.tsx"; -import React, { useReducer, useCallback, useEffect, useState } from "react"; -import OpportunitiesList from "./OpportunitiesDetails.tsx"; +import OpportunitiesList from "./OpportunitiesList.tsx"; import PopUpMenu from "./PopUpMenu.tsx"; -import { Filters, OpportunityList } from "../../types/opportunities.ts"; +import { useOpportunity } from "../../context/useOpportunity.tsx"; const Posts = () => { const [popUpMenu, setPopUpMenu] = useState(false); - - const date = new Date(); - const currYr = date.getFullYear(); // replace currYr with user year - interface OpportunityState { - filters: { - activeFilters: string[]; - filterMap: Filters; - }; - activeId: string; - opportunities: OpportunityList[]; - } - - type OpportunityAction = - | { type: "SET_FILTERS"; activeFilters: string[]; filterMap: Filters } - | { type: "RESET_FILTERS" } - | { type: "REMOVE_FILTER"; filter: string } - | { type: "SET_OPPORTUNITIES"; opportunities: OpportunityList[] }; - - const reducer: React.Reducer = (state, action) => { - switch (action.type) { - case "SET_FILTERS": - if (!action.activeFilters || !action.filterMap) return state; - - return { - ...state, - filters: { - activeFilters: action.activeFilters, - filterMap: action.filterMap, - }, - }; - - case "RESET_FILTERS": { - return { - ...state, - filters: { - activeFilters: [currYr.toString()], - filterMap: { - years: [currYr], - credits: [], - hourlyPay: 0, - majors: [], - }, - }, - }; - } - - case "REMOVE_FILTER": { - if (!action.filter) return state; - - const newActiveFilters = state.filters.activeFilters.filter((filter) => filter !== action.filter); - const newFilterMap = { - ...state.filters.filterMap, - years: state.filters.filterMap.years.filter((year) => year !== parseInt(action.filter)), - credits: action.filter.includes("Credit") ? [] : state.filters.filterMap.credits, - majors: state.filters.filterMap.majors.filter((major) => major !== action.filter), - hourlyPay: action.filter.includes("$") ? 0 : state.filters.filterMap.hourlyPay, - }; - - return { - ...state, - filters: { - activeFilters: newActiveFilters, - filterMap: newFilterMap, - }, - }; - } - - case "SET_OPPORTUNITIES": - if (!action.opportunities) return state; - - return { - ...state, - opportunities: action.opportunities, - }; - - default: - return state; - } - }; - - const [opportunityState, dispatch] = useReducer(reducer, { - filters: { - activeFilters: [currYr.toString()], - filterMap: { - years: [currYr], - credits: [], - hourlyPay: 0, - majors: [], - }, - }, - activeId: "", - opportunities: [], - }); - - // Action dispatchers - const resetFilters = useCallback(() => { - dispatch({ type: "RESET_FILTERS" }); - }, []); - - const removeFilter = useCallback((name: string) => { - dispatch({ type: "REMOVE_FILTER", filter: name }); - }, []); - - const setFilters = useCallback((activeFilters: string[], filterMap: Filters) => { - dispatch({ type: "SET_FILTERS", activeFilters, filterMap }); - }, []); + const { filterMap, setOpportunities } = useOpportunity(); + const fetchOpportunities = useCallback(async () => { const queryParams = new URLSearchParams( - Object.entries(opportunityState.filters.filterMap) + Object.entries(filterMap) .filter(([, value]) => { if (Array.isArray(value)) return value.length > 0; return value !== 0 && value !== null && value !== undefined; @@ -142,17 +38,16 @@ const Posts = () => { if (!response.ok) { console.log("Error fetching opportunities", response.status); - dispatch({ type: "SET_OPPORTUNITIES", opportunities: [] }); + setOpportunities([]); } else { const data = await response.json(); - dispatch({ type: "SET_OPPORTUNITIES", opportunities: data }); - console.log(data); + setOpportunities(data); } } catch (error) { console.error("Error fetching opportunities:", error); - dispatch({ type: "SET_OPPORTUNITIES", opportunities: [] }); + setOpportunities([]); } - }, [opportunityState.filters.filterMap, dispatch]); + }, [filterMap, setOpportunities]); useEffect(() => { fetchOpportunities(); @@ -160,9 +55,9 @@ const Posts = () => { return (
- setPopUpMenu(!popUpMenu)} /> - {popUpMenu && setPopUpMenu(!popUpMenu)} filters={opportunityState.filters.filterMap} reset={resetFilters} setFilters={setFilters} />} - + setPopUpMenu(!popUpMenu)} /> + {popUpMenu && setPopUpMenu(!popUpMenu)} />} +
); }; diff --git a/src/opportunities/components/SearchBar.tsx b/src/opportunities/components/SearchBar.tsx deleted file mode 100644 index 5a22f97f..00000000 --- a/src/opportunities/components/SearchBar.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from "react"; -import { CiSearch } from "react-icons/ci"; - -const SearchBar = () => { - return ( -
- - - - ); -}; - -export default SearchBar; diff --git a/src/opportunities/pages/Opportunities.tsx b/src/opportunities/pages/Opportunities.tsx index f862ec3e..ecbed47a 100644 --- a/src/opportunities/pages/Opportunities.tsx +++ b/src/opportunities/pages/Opportunities.tsx @@ -1,6 +1,8 @@ import React from "react"; import Posts from "../components/Posts"; import usePageNavigation from "../../shared/hooks/page-navigation-hook.ts"; +import SavedPage from "../../individuals/pages/Saved.tsx"; +import { OpportunityProvider } from "../../context/OpportunityContext.tsx"; interface PageNavigationType { activePage: string; @@ -20,34 +22,37 @@ const Opportunities: React.FC = () => { // displaying opportunities list component return ( -
-
-
-
-

Opportunities

- - -
- - {pages.activePage === "Search" && } - + +
+
+
+
+

Opportunities

+ + +
+ + {pages.activePage === "Search" && } + {pages.activePage === "Saved" && } + +
-
+ ); }; diff --git a/src/shared/components/Checkbox.tsx b/src/shared/components/Checkbox.tsx index cbffadd5..623d02a7 100644 --- a/src/shared/components/Checkbox.tsx +++ b/src/shared/components/Checkbox.tsx @@ -1,6 +1,6 @@ import React from "react"; import { FieldErrors, UseFormRegisterReturn } from "react-hook-form"; -import { Filters } from "../../types/opportunities.ts"; +import { Filters } from "../../types/filters.ts"; interface CheckBoxProps { formHook: UseFormRegisterReturn; @@ -53,7 +53,7 @@ export default function CheckBox({ {...formHook} id={item} className={type === "radio" ? "radio" : "checkbox"} - defaultChecked={(name === "semesters" && filters?.semesters?.includes(item)) || (name === "years" && filters?.years?.includes(Number(item))) || (name === "credits" && filters?.credits?.includes(item) ? true : false)} + defaultChecked={(name === "years" && filters?.years?.includes(Number(item))) || (name === "credits" && filters?.credits?.includes(item) ? true : false)} />
diff --git a/src/shared/components/Navigation/MainNavigation.tsx b/src/shared/components/Navigation/MainNavigation.tsx index 7805cfd6..a91dc020 100644 --- a/src/shared/components/Navigation/MainNavigation.tsx +++ b/src/shared/components/Navigation/MainNavigation.tsx @@ -25,7 +25,6 @@ export default function MainNavigation() { { name: "Create", href: "/create" }, { name: "Staff", href: "/staff" }, { name: "Profile", href: "/profile" }, - { name: "Saved", href: "/saved" }, { name: "Sign Out", href: "/signout" } ] : [{ name: "Sign In", href: "/signin" }]; diff --git a/src/types/filters.ts b/src/types/filters.ts new file mode 100644 index 00000000..e587b01d --- /dev/null +++ b/src/types/filters.ts @@ -0,0 +1,6 @@ +export type Filters = { + years: number[]; + credits: string[]; + hourlyPay: number; + majors: string[]; +}; \ No newline at end of file diff --git a/src/types/opportunities.ts b/src/types/opportunities.ts deleted file mode 100644 index 229fd275..00000000 --- a/src/types/opportunities.ts +++ /dev/null @@ -1,21 +0,0 @@ -export type Filters = { - years: number[]; - credits: string[]; - hourlyPay: number; - majors: string[]; -}; - -export type OpportunityList = { - id: number; - name: string; - description: string; - recommended_experience: string; - pay: number; - credits: string; - semester: string; - year: number; - application_due: Date; - location: string; - lab_managers: string - saved: boolean; -} \ No newline at end of file diff --git a/src/types/opportunity.ts b/src/types/opportunity.ts index 48fc2ebd..e59004b3 100644 --- a/src/types/opportunity.ts +++ b/src/types/opportunity.ts @@ -1,13 +1,14 @@ export type Opportunity = { - id: string; - name: string; - description: string; - recommended_experience?: string; - pay?: number; - credits?: string; - semester: string; - year: number; - application_due: string; - active: boolean; - location: string; + id: number; + name: string; + description: string; + recommended_experience: string; + pay: number; + credits: string; + semester: string; + year: number; + application_due: Date; + location: string; + lab_managers: string; + saved: boolean; } \ No newline at end of file diff --git a/src/types/opportunityaction.ts b/src/types/opportunityaction.ts new file mode 100644 index 00000000..185ca5e9 --- /dev/null +++ b/src/types/opportunityaction.ts @@ -0,0 +1,9 @@ +import { Filters } from "./filters.ts"; +import { Opportunity } from "./opportunity.ts"; + +export type OpportunityAction = + | { type: "SET_QUERY"; query: string } + | { type: "SET_FILTERS"; activeFilters: string[]; filterMap: Filters } + | { type: "RESET_FILTERS" } + | { type: "REMOVE_FILTER"; filter: string } + | { type: "SET_OPPORTUNITIES"; opportunities: Opportunity[] }; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index ec1d7313..38bb3e55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true,
NameDescriptionRecommended ExperiencePayCreditsSemesterYearApplication DueLocationUnsave
{opportunity.name}{opportunity.description}{opportunity.recommended_experience}{opportunity.pay}{opportunity.credits}{opportunity.semester}{opportunity.year} { - const today = new Date(); - const dueDate = new Date(opportunity.application_due); - const oneWeek = 7 * 24 * 60 * 60 * 1000; + + + + + + + + + + + + + + + + + {saved.map((opportunity) => ( + + + + + + + + + - - + + - - ))} + setSaved(prev => prev ? prev.filter(o => o.id !== opportunity.id) : prev); + } catch { + console.log("Error unsaving opportunity"); + } + }} + > + Unsave + + + + ))} +
NameDescriptionRecommended ExperiencePayCreditsSemesterYearApplication DueLocationUnsave
{opportunity.name}{opportunity.description}{opportunity.recommended_experience}{opportunity.pay}{opportunity.credits}{opportunity.semester}{opportunity.year} { + const today = new Date(); + const dueDate = new Date(opportunity.application_due); + const oneWeek = 7 * 24 * 60 * 60 * 1000; - if (dueDate < today) { - return "red"; - } else if (dueDate.getTime() - today.getTime() <= oneWeek) { - return "orange"; - } else { - return "black"; - } - })() - }}> - {new Date(opportunity.application_due).toLocaleDateString("en-US")} - {opportunity.location} - {opportunity.location} + -
)} - + + ); }; diff --git a/src/opportunities/components/FiltersField.tsx b/src/opportunities/components/FiltersField.tsx index cf049855..c64cbe49 100644 --- a/src/opportunities/components/FiltersField.tsx +++ b/src/opportunities/components/FiltersField.tsx @@ -1,25 +1,35 @@ import React from "react"; import SmallTextButton from "./SmallTextButton.tsx"; -import SearchBar from "./SearchBar.tsx"; import GroupedComponents from "../../shared/components/UIElements/GroupedComponents.jsx"; import HorizontalIconButton from "./HorizontalIconButton.tsx"; import { PiSlidersHorizontal } from "react-icons/pi"; import { MdCancel } from "react-icons/md"; +import { CiSearch } from "react-icons/ci"; +import { useOpportunity } from "../../context/useOpportunity.tsx"; interface FiltersFieldProps { - resetFilters: () => void; - deleteFilter: (filter: string) => void; - filters: string[]; setPopUpMenu: () => void; } -export default function FiltersField({ resetFilters, deleteFilter, filters, setPopUpMenu }: FiltersFieldProps) { + +export default function FiltersField({ setPopUpMenu }: FiltersFieldProps) { + const { activeFilters, resetFilters, removeFilter, setQuery } = useOpportunity(); + const [newQuery, setNewQuery] = React.useState(""); return (

- +
{ e.preventDefault(); setQuery(newQuery); }} className="flex p-2 px-3 border rounded-3xl align-items-center border-gray-400" > + setNewQuery(e.target.value)} + /> + + @@ -29,10 +39,10 @@ export default function FiltersField({ resetFilters, deleteFilter, filters, setP {/* Fix rendering with new filters = [ [],[],[] ]*/} - {filters.map((filter) => { + {activeFilters.map((filter) => { return ( removeFilter(filter)} icon={} key={filter} special={false} diff --git a/src/opportunities/components/OpportunitiesDetails.tsx b/src/opportunities/components/OpportunitiesList.tsx similarity index 53% rename from src/opportunities/components/OpportunitiesDetails.tsx rename to src/opportunities/components/OpportunitiesList.tsx index d4e41f36..e879f71b 100644 --- a/src/opportunities/components/OpportunitiesDetails.tsx +++ b/src/opportunities/components/OpportunitiesList.tsx @@ -1,12 +1,69 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { Link } from "react-router-dom"; -import { OpportunityList } from "../../types/opportunities.ts"; +import { Opportunity } from "../../types/opportunity.ts"; +import { OpportunityAction } from "../../types/opportunityaction.ts"; +import { getCookie } from "../../utils.ts"; +import { useOpportunity } from "../../context/useOpportunity.tsx"; -interface OpportunitiesListProps { - opportunities: OpportunityList[]; -} +export default function OpportunitiesList() { + + const csrfToken = getCookie('csrf_access_token'); + const { opportunities, setOpportunities } = useOpportunity(); + + function toggleSave(opportunity: Opportunity) { + console.log(opportunities) + const updated = opportunities.map((item: Opportunity) => + item.id === opportunity.id + ? { ...item, saved: !item.saved } + : item + ); + console.log(updated) + + setOpportunities(updated); + } + console.log(opportunities); + + async function changeSavedOpportunity(opportunity: Opportunity) { + console.log("Current saved state:", opportunity.saved); + + const headers: Record = { + "Content-Type": "application/json", + }; + if (csrfToken) headers["X-CSRF-TOKEN"] = csrfToken; + + const isSaving = !opportunity.saved; + const endpoint = isSaving + ? `${import.meta.env.VITE_BACKEND_SERVER}/saveOpportunity/${opportunity.id}` + : `${import.meta.env.VITE_BACKEND_SERVER}/unsaveOpportunity/${opportunity.id}`; + const method = isSaving ? "POST" : "DELETE"; + + try { + console.log(`Attempting to ${isSaving ? "save" : "unsave"} opportunity...`); + + const response = await fetch(endpoint, { + method, + credentials: "include", + headers, + body: JSON.stringify({}), // safe for POST, harmless for DELETE + }); + + if (!response.ok) { + throw new Error(`Failed to ${isSaving ? "save" : "unsave"} opportunity`); + } + + console.log(`Successfully ${isSaving ? "saved" : "unsaved"} opportunity.`); + } catch (error) { + console.error( + `Error ${isSaving ? "saving" : "unsaving"} opportunity:`, + error + ); + } finally { + // Optimistically update local UI + toggleSave(opportunity); + console.log("Local save state toggled."); + } + } -export default function OpportunitiesList({ opportunities }: OpportunitiesListProps) { return (
@@ -47,7 +104,7 @@ export default function OpportunitiesList({ opportunities }: OpportunitiesListPr
-