- | {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;
+
+
+
+ | Name |
+ Description |
+ Recommended Experience |
+ Pay |
+ Credits |
+ Semester |
+ Year |
+ Application Due |
+ Location |
+ Unsave |
+
+
+
+ {saved.map((opportunity) => (
+
+ | {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} |
+
+
- |
-
- ))}
+ setSaved(prev => prev ? prev.filter(o => o.id !== opportunity.id) : prev);
+ } catch {
+ console.log("Error unsaving opportunity");
+ }
+ }}
+ >
+ Unsave
+
+
+
+ ))}
+
)}
-
+
+
);
};
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 (
-
+
@@ -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
|
- |
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
- Cancel
+ Cancel
- { reset(); setFunction(); }} className="btn btn-primary border-black text-gray-700 bg-white w-1/2 hover:text-gray-900 hover:bg-gray-200 hover:border-black focus:text-gray-900 focus:bg-gray-100 focus:border-black">Reset
+ { resetFilters(); setOpen(); }} className="btn btn-primary border-black text-gray-700 bg-white w-1/2 hover:text-gray-900 hover:bg-gray-200 hover:border-black focus:text-gray-900 focus:bg-gray-100 focus:border-black">Reset
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,