Skip to content

Commit 5e47804

Browse files
Complete filtering of opportunities (#129)
2 parents 5768041 + 0bdb989 commit 5e47804

20 files changed

+557
-590
lines changed

src/App.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,20 @@ import React from "react";
22
import { Routes, Route } from "react-router-dom";
33
import "./style/App.css";
44

5-
import Opportunities from "./opportunities/pages/opportunities.tsx";
6-
75
import Home from "./shared/pages/Home.tsx";
86
import PageNotFound from "./shared/pages/404.tsx";
97
import MainNavigation from "./shared/components/Navigation/MainNavigation.tsx";
10-
import Jobs from "./opportunities/pages/Jobs.tsx";
8+
import StickyFooter from "./shared/components/Navigation/StickyFooter.tsx";
9+
import ProfilePage from "./shared/pages/Profile.tsx";
1110
import Departments from "./staff/pages/Departments.tsx";
1211
import StaffPage from "./staff/pages/Staff.tsx";
1312
import Department from "./staff/pages/Department.tsx";
1413
import CreatePost from "./staff/pages/CreatePost.tsx";
15-
import IndividualPost from "./opportunities/pages/IndividualPost.tsx";
16-
import ProfilePage from "./shared/pages/Profile.tsx";
1714
import LoginRedirection from "./auth/Login.tsx";
1815
import LogoutRedirection from "./auth/Logout.tsx";
19-
import StickyFooter from "./shared/components/Navigation/StickyFooter.tsx";
2016
import Token from "./auth/Token.tsx";
17+
import Opportunities from "./opportunities/pages/Opportunities.tsx";
18+
import IndividualPost from "./opportunities/pages/IndividualPost.tsx";
2119
import { HelmetProvider } from 'react-helmet-async';
2220
import { AuthProvider } from './context/AuthContext.tsx';
2321

@@ -37,10 +35,8 @@ function App() {
3735
<Route path="/login" element={<LoginRedirection />} />
3836
<Route path="/signout" element={<LogoutRedirection />} />
3937
<Route path="/logout" element={<LogoutRedirection />} />
40-
41-
<Route path="/opportunities" element={<Opportunities />} />
4238

43-
<Route path="/jobs" element={<Jobs />} />
39+
<Route path="/opportunities" element={<Opportunities />} />
4440
<Route path="/profile" element={<ProfilePage />} />
4541
<Route
4642
path="/staff/department/:department"

src/opportunities/components/FiltersField.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,15 @@ import GroupedComponents from "../../shared/components/UIElements/GroupedCompone
55
import HorizontalIconButton from "./HorizontalIconButton.tsx";
66
import { PiSlidersHorizontal } from "react-icons/pi";
77
import { MdCancel } from "react-icons/md";
8-
import PropTypes from "prop-types";
98

10-
const FiltersField = ({ resetFilters, deleteFilter, filters, setPopUpMenu }) => {
11-
9+
interface FiltersFieldProps {
10+
resetFilters: () => void;
11+
deleteFilter: (filter: string) => void;
12+
filters: string[];
13+
setPopUpMenu: () => void;
14+
}
15+
16+
export default function FiltersField({ resetFilters, deleteFilter, filters, setPopUpMenu }: FiltersFieldProps) {
1217
return (
1318
<div>
1419
<hr />
@@ -17,15 +22,15 @@ const FiltersField = ({ resetFilters, deleteFilter, filters, setPopUpMenu }) =>
1722
<SearchBar />
1823

1924
<SmallTextButton className="" onClick={setPopUpMenu} special={true}>
20-
<PiSlidersHorizontal className="pr-1"/>
25+
<PiSlidersHorizontal className="pr-1" />
2126
Change Filters
22-
<PiSlidersHorizontal className="pl-1"/>
27+
<PiSlidersHorizontal className="pl-1" />
2328
</SmallTextButton>
2429

2530
{/* Fix rendering with new filters = [ [],[],[] ]*/}
2631
<GroupedComponents gap={2}>
27-
{filters[1].map((filter) => {
28-
return(
32+
{filters.map((filter) => {
33+
return (
2934
<HorizontalIconButton
3035
onClick={deleteFilter}
3136
icon={<MdCancel />}
@@ -47,12 +52,3 @@ const FiltersField = ({ resetFilters, deleteFilter, filters, setPopUpMenu }) =>
4752
</div>
4853
);
4954
};
50-
51-
FiltersField.propTypes = {
52-
resetFilters: PropTypes.func.isRequired,
53-
deleteFilter: PropTypes.func.isRequired,
54-
filters: PropTypes.arrayOf(PropTypes.array),
55-
setPopUpMenu: PropTypes.func.isRequired,
56-
};
57-
58-
export default FiltersField;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React from "react";
2+
import { OpportunityList } from "../../types/opportunities.ts";
3+
4+
interface OpportunitiesListProps {
5+
opportunities: OpportunityList[];
6+
}
7+
8+
export default function OpportunitiesList({ opportunities }: OpportunitiesListProps) {
9+
return (
10+
<div className="p-4">
11+
<div className="overflow-x-auto">
12+
<table className="w-full border-collapse">
13+
<thead>
14+
{/* Column Headers */}
15+
<tr className="bg-gray-100">
16+
<th className="p-3 text-left border">Position</th>
17+
<th className="p-3 text-left border">Description</th>
18+
<th className="p-3 text-left border">Location</th>
19+
<th className="p-3 text-left border">Pay</th>
20+
<th className="p-3 text-left border">Credits</th>
21+
<th className="p-3 text-left border">Lab Managers</th>
22+
<th className="p-3 text-left border">Term</th>
23+
<th className="p-3 text-left border">View</th>
24+
<th className="p-3 text-left border">Save</th>
25+
</tr>
26+
</thead>
27+
<tbody>
28+
{/* Info about the opportunities */}
29+
{opportunities.length > 0 ? (
30+
opportunities.map((opportunity) => (
31+
<tr key={opportunity.id} className="hover:bg-gray-50">
32+
<td className="p-3 border font-medium">{opportunity.name}</td>
33+
<td className="p-3 border">{opportunity.description}</td>
34+
<td className="p-3 border">{opportunity.location}</td>
35+
<td className="p-3 border">{opportunity.pay ? `$${opportunity.pay}/hr` : ""}</td>
36+
<td className="p-3 border">{opportunity.credits}</td>
37+
<td className="p-3 border">{opportunity.lab_managers}</td>
38+
<td className="p-3 border">
39+
{opportunity.semester} {opportunity.year}
40+
</td>
41+
<td className="p-3 border">
42+
<button className="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-600 mr-2">
43+
View
44+
</button>
45+
</td>
46+
<td className="p-3 border">
47+
<button className="bg-blue-500 text-white px-4 py-1 rounded hover:bg-blue-600">
48+
{opportunity.saved ? "Unsave" : "Save"}
49+
</button>
50+
</td>
51+
</tr>
52+
))
53+
) : (
54+
<tr>
55+
<td colSpan={9} className="p-3 border text-center">
56+
No results found.
57+
</td>
58+
</tr>
59+
)}
60+
</tbody>
61+
</table>
62+
</div>
63+
</div>
64+
);
65+
};
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
import React, { useEffect, useState } from "react";
2+
import { useForm } from "react-hook-form";
3+
import CheckBox from "../../shared/components/Checkbox.tsx";
4+
import Input from "../../staff/components/Input";
5+
import { Filters } from "../../types/opportunities.ts";
6+
7+
interface PopUpMenuProps {
8+
setFunction: () => void;
9+
reset: () => void;
10+
filters: Filters;
11+
setFilters: (activeFilters: string[], filterMap: Filters) => void;
12+
}
13+
14+
interface Major {
15+
code: string;
16+
name: string;
17+
}
18+
19+
export default function PopUpMenu({ setFunction, reset, filters, setFilters }: PopUpMenuProps) {
20+
const [majors, setMajors] = useState<Major[]>();
21+
const [validYears, setValidYears] = useState<string[]>([]);
22+
23+
const checkboxes: [string, string[], "years" | "credits"][] = [
24+
["Class Year", validYears, "years"],
25+
["Credits", ["1", "2", "3", "4"], "credits"]
26+
];
27+
28+
useEffect(() => {
29+
const fetchMajors = async () => {
30+
const url = `${process.env.REACT_APP_BACKEND_SERVER}/majors`;
31+
const response = await fetch(url);
32+
if (!response.ok) {
33+
console.log("Error fetching majors");
34+
} else {
35+
const data = await response.json();
36+
setMajors(data);
37+
}
38+
}
39+
fetchMajors();
40+
}, []);
41+
42+
useEffect(() => {
43+
const fetchYears = async () => {
44+
const url = `${process.env.REACT_APP_BACKEND_SERVER}/years`;
45+
const response = await fetch(url);
46+
if (!response.ok) {
47+
console.log("Error fetching valid years");
48+
} else {
49+
const data = await response.json();
50+
setValidYears(data);
51+
}
52+
}
53+
fetchYears();
54+
}, []);
55+
56+
const {
57+
register,
58+
handleSubmit,
59+
formState: { errors },
60+
} = useForm({
61+
defaultValues: {
62+
years: [],
63+
credits: [],
64+
hourlyPay: filters.hourlyPay ?? 0,
65+
majors: []
66+
},
67+
});
68+
69+
interface FormData {
70+
years: string[],
71+
credits: string[],
72+
hourlyPay: number,
73+
majors: string[]
74+
}
75+
76+
function formatCredits(credits: string[]): string | null {
77+
if (credits.length === 4) {
78+
return "1-4 Credits";
79+
} else if (credits.length === 1) {
80+
return `${credits[0]} ${credits[0] === "1" ? "Credit" : "Credits"}`;
81+
} else if (JSON.stringify(credits) === JSON.stringify(["1", "2", "3"])) {
82+
return "1-3 Credits";
83+
} else if (JSON.stringify(credits) === JSON.stringify(["2", "3", "4"])) {
84+
return "2-4 Credits";
85+
} else if (credits.length === 0) {
86+
return null;
87+
} else {
88+
return `${credits.join(", ")} Credits`;
89+
}
90+
}
91+
92+
function submitHandler(data: FormData) {
93+
const { years, credits, hourlyPay, majors } = data;
94+
const newFilterMap: Filters = {
95+
years: years.map(Number),
96+
credits: credits,
97+
hourlyPay: Number(hourlyPay),
98+
majors: majors
99+
}
100+
101+
const activeFilters: string[] = [
102+
...years,
103+
...(formatCredits(credits) ? [formatCredits(credits)!] : []),
104+
...(Number(hourlyPay) > 0 ? [`$${Number(hourlyPay).toFixed(2)}/hr+`] : []),
105+
...majors
106+
];
107+
setFilters(activeFilters, newFilterMap);
108+
setFunction()
109+
};
110+
111+
console.log("Filters: ", filters);
112+
113+
return (
114+
<section className="relative z-10" aria-labelledby="modal-title" role="dialog" aria-modal="true">
115+
<div className="fixed inset-0 bg-gray-500/75 transition-opacity" aria-hidden="true"></div>
116+
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
117+
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
118+
<div className="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-6xl">
119+
<div className="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 border-4">
120+
<div className="text-2xl font-semibold text-center pb-3">Filters</div>
121+
<section className="flex flex-col">
122+
<form
123+
onSubmit={handleSubmit((data) => {
124+
submitHandler(data);
125+
})}
126+
className="form-container"
127+
> <section className="flex flex-col max-h[100] overflow-y-auto"> {/* Added max-height and overflow-y-auto */}
128+
<section className="flex justify-center">
129+
{checkboxes.map((filter) => (
130+
<div className="w-1/3" key={filter[2]}>
131+
<CheckBox
132+
errors={errors}
133+
errorMessage={filter[2] + " checkbox failed"}
134+
label={filter[0]}
135+
options={filter[1]}
136+
formHook={{ ...register(filter[2], {}) }}
137+
name={filter[2]}
138+
type="checkbox"
139+
filters={filters}
140+
/>
141+
</div>
142+
))}
143+
</section>
144+
<section className="flex justify-center">
145+
<Input
146+
errors={errors}
147+
label="Minimum Hourly Pay"
148+
name={"hourlyPay"}
149+
errorMessage={"Hourly pay must be at least 0"}
150+
formHook={{
151+
...register("hourlyPay", {
152+
required: "Hourly pay is required",
153+
validate: value => value >= 0 || "Hourly pay must be greater or equal to 0",
154+
pattern: {
155+
value: /^[0-9]\d*$/,
156+
message: "Hourly pay must be a positive integer"
157+
}
158+
})
159+
}}
160+
type="number"
161+
options={[]}
162+
placeHolder="Enter minimum hourly pay"
163+
/>
164+
</section>
165+
166+
<section className="pt-7 flex flex-col justify-center">
167+
<h1 className="font-semibold text-lg text-center">Majors</h1>
168+
<section className="flex justify-center py-4">
169+
<select
170+
multiple
171+
size={5}
172+
{...register("majors", {})}
173+
className="form-multiselect block w-3/4 border-gray-300 rounded-md shadow-lg focus:border-blue-500 focus:ring focus:ring-blue-300 focus:ring-opacity-50 bg-white text-gray-700"
174+
>
175+
{majors &&
176+
majors.map((major, index) => (
177+
<option
178+
key={index}
179+
value={major.code}
180+
className="py-2 px-3 hover:bg-blue-100"
181+
selected={filters.majors.includes(major.code)}
182+
>
183+
{major.name}
184+
</option>
185+
))}
186+
</select>
187+
</section>
188+
</section>
189+
</section>
190+
191+
<section className="flex flex-row justify-center">
192+
<div className="w-1/3 flex justify-center">
193+
<button type="button" onClick={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">Cancel</button>
194+
</div>
195+
<div className="w-1/3 flex justify-center">
196+
<button type="button" onClick={() => { 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</button>
197+
</div>
198+
<div className="w-1/3 flex justify-center">
199+
<input type="submit" value="Search" className="btn btn-primary bg-blue-700 text-gray-100 w-1/2 hover:bg-blue-800 focus:bg-blue-800" />
200+
</div>
201+
</section>
202+
203+
</form>
204+
</section>
205+
</div>
206+
</div>
207+
</div>
208+
</div>
209+
</section>
210+
);
211+
}

0 commit comments

Comments
 (0)