Skip to content

Commit 2792f77

Browse files
authored
Doan frontend2 (#171)
Co-authored-by: doan-neyugn <[email protected]> Co-authored-by: Will Broadwell <[email protected]> Dark mode css changes and small stylistic changes sitewide
1 parent 190d943 commit 2792f77

File tree

12 files changed

+786
-448
lines changed

12 files changed

+786
-448
lines changed

src/individuals/pages/Saved.tsx

Lines changed: 189 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,207 @@
1-
import React, { useEffect } from "react";
2-
import { useState } from "react";
1+
import React, { useEffect, useState } from "react";
32
import { useAuth } from "../../context/AuthContext.tsx";
43
import { Opportunity } from "../../types/opportunity.ts";
54
import { getCookie } from "../../utils.ts";
65

76
export default function SavedPage() {
8-
const { auth } = useAuth();
7+
const { auth } = useAuth();
98

10-
if (!auth.isAuthenticated) {
11-
window.location.href = "/login";
12-
}
9+
if (!auth.isAuthenticated) {
10+
window.location.href = "/login";
11+
}
1312

14-
const [saved, setSaved] = useState<null | Opportunity[]>(null);
13+
const [saved, setSaved] = useState<Opportunity[] | null>(null);
1514

16-
const csrfToken = getCookie('csrf_access_token');
15+
const csrfToken = getCookie("csrf_access_token");
1716

18-
const fetchSaved = async () => {
19-
try {
20-
const response = await fetch(
21-
`${import.meta.env.VITE_BACKEND_SERVER}/savedOpportunities`, {
22-
credentials: "include",
23-
}
24-
);
17+
const fetchSaved = async () => {
18+
try {
19+
const response = await fetch(
20+
`${import.meta.env.VITE_BACKEND_SERVER}/savedOpportunities`,
21+
{
22+
credentials: "include",
23+
}
24+
);
2525

26-
if (!response.ok) {
27-
throw new Error("Saved not found");
28-
}
26+
if (!response.ok) {
27+
throw new Error("Saved not found");
28+
}
2929

30-
const data = await response.json();
31-
setSaved(data);
32-
console.log(data);
33-
} catch {
34-
console.log("Error fetching saved");
35-
}
30+
const data = await response.json();
31+
setSaved(data);
32+
} catch {
33+
console.log("Error fetching saved");
3634
}
35+
};
36+
37+
useEffect(() => {
38+
fetchSaved();
39+
}, []);
40+
41+
return (
42+
<section className="w-full px-4 py-8 md:px-8 lg:px-10">
43+
<div className="max-w-6xl mx-auto">
44+
<h1 className="text-center my-4 text-3xl md:text-4xl font-extrabold tracking-tight text-gray-900 dark:text-gray-100">
45+
Saved Opportunities
46+
</h1>
47+
48+
{saved === null && (
49+
<p className="mt-6 text-center text-gray-500 dark:text-gray-400">
50+
Loading...
51+
</p>
52+
)}
3753

38-
useEffect(() => {
39-
fetchSaved();
40-
}, []);
41-
42-
return (
43-
<section className="center container-xl">
44-
<h1 className="text-center my-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl">
45-
Saved Opportunities
46-
</h1>
47-
{!saved && "Loading..."}
48-
{saved && (
49-
<table>
50-
<tr>
51-
<th>Name</th>
52-
<th>Description</th>
53-
<th>Recommended Experience</th>
54-
<th>Pay</th>
55-
<th>Credits</th>
56-
<th>Semester</th>
57-
<th>Year</th>
58-
<th>Application Due</th>
59-
<th>Location</th>
60-
<th>Unsave</th>
54+
{saved !== null && (
55+
<div className="mt-6 bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 p-4">
56+
{saved.length === 0 ? (
57+
<p className="text-center text-gray-500 dark:text-gray-400 py-4">
58+
You don’t have any saved opportunities yet.
59+
</p>
60+
) : (
61+
<div className="overflow-x-auto">
62+
<table className="w-full border-collapse text-sm md:text-base">
63+
<thead className="top-0 z-10">
64+
<tr className="bg-gray-100 dark:bg-gray-800/80 backdrop-blur">
65+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
66+
Name
67+
</th>
68+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
69+
Description
70+
</th>
71+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
72+
Recommended Experience
73+
</th>
74+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
75+
Pay
76+
</th>
77+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
78+
Credits
79+
</th>
80+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
81+
Semester
82+
</th>
83+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
84+
Year
85+
</th>
86+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
87+
Application Due
88+
</th>
89+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
90+
Location
91+
</th>
92+
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
93+
Unsave
94+
</th>
6195
</tr>
62-
{saved.map((opportunity) => (
63-
<tr key={opportunity.id}>
64-
<td>{opportunity.name}</td>
65-
<td>{opportunity.description}</td>
66-
<td>{opportunity.recommended_experience}</td>
67-
<td>{opportunity.pay}</td>
68-
<td>{opportunity.credits}</td>
69-
<td>{opportunity.semester}</td>
70-
<td>{opportunity.year}</td>
71-
<td style={{
72-
color: (() => {
73-
const today = new Date();
74-
const dueDate = new Date(opportunity.application_due);
75-
const oneWeek = 7 * 24 * 60 * 60 * 1000;
76-
77-
if (dueDate < today) {
78-
return "red";
79-
} else if (dueDate.getTime() - today.getTime() <= oneWeek) {
80-
return "orange";
81-
} else {
82-
return "black";
96+
</thead>
97+
98+
<tbody className="[&_tr:nth-child(even)]:bg-transparent dark:[&_tr:nth-child(even)]:bg-gray-800/30">
99+
{saved.map((opportunity) => {
100+
const today = new Date();
101+
const dueDate = new Date(opportunity.application_due);
102+
const oneWeek = 7 * 24 * 60 * 60 * 1000;
103+
104+
let dueClass =
105+
"p-3 border border-gray-300 dark:border-gray-600";
106+
if (dueDate < today) {
107+
dueClass += " text-red-500 font-semibold";
108+
} else if (
109+
dueDate.getTime() - today.getTime() <= oneWeek
110+
) {
111+
dueClass += " text-orange-400 font-semibold";
112+
}
113+
114+
return (
115+
<tr
116+
key={opportunity.id}
117+
className="hover:bg-gray-50 dark:hover:bg-gray-800/60 transition-colors"
118+
>
119+
<td className="p-3 border border-gray-300 dark:border-gray-600 font-medium">
120+
{opportunity.name}
121+
</td>
122+
<td className="p-3 border border-gray-300 dark:border-gray-600">
123+
{opportunity.description}
124+
</td>
125+
<td className="p-3 border border-gray-300 dark:border-gray-600">
126+
{opportunity.recommended_experience}
127+
</td>
128+
<td className="p-3 border border-gray-300 dark:border-gray-600">
129+
{opportunity.pay
130+
? `$${opportunity.pay}/hr`
131+
: ""}
132+
</td>
133+
<td className="p-3 border border-gray-300 dark:border-gray-600">
134+
{opportunity.credits}
135+
</td>
136+
<td className="p-3 border border-gray-300 dark:border-gray-600">
137+
{opportunity.semester}
138+
</td>
139+
<td className="p-3 border border-gray-300 dark:border-gray-600">
140+
{opportunity.year}
141+
</td>
142+
<td className={dueClass}>
143+
{new Date(
144+
opportunity.application_due
145+
).toLocaleDateString("en-US")}
146+
</td>
147+
<td className="p-3 border border-gray-300 dark:border-gray-600">
148+
{opportunity.location}
149+
</td>
150+
<td className="p-3 border border-gray-300 dark:border-gray-600">
151+
<button
152+
className="bg-blue-600 dark:bg-blue-700 text-white px-3 py-1 rounded hover:bg-blue-700 dark:hover:bg-blue-800 focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm"
153+
onClick={async () => {
154+
try {
155+
const headers: Record<string, string> = {
156+
"Content-Type": "application/json",
157+
};
158+
if (csrfToken) {
159+
headers["X-CSRF-TOKEN"] = csrfToken;
160+
}
161+
162+
const response = await fetch(
163+
`${import.meta.env
164+
.VITE_BACKEND_SERVER}/unsaveOpportunity/${
165+
opportunity.id
166+
}`,
167+
{
168+
method: "DELETE",
169+
credentials: "include",
170+
headers,
83171
}
84-
})()
85-
}}>
86-
{new Date(opportunity.application_due).toLocaleDateString("en-US")}
87-
</td>
88-
<td>{opportunity.location}</td>
89-
<td>
90-
<button className="p-2 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500"
91-
onClick={async () => {
92-
try {
93-
const headers: Record<string, string> = {
94-
"Content-Type": "application/json", // Good practice for cross-origin requests
95-
};
96-
if (csrfToken) {
97-
headers["X-CSRF-TOKEN"] = csrfToken; // Include the token only when defined
98-
}
99-
100-
const response = await fetch(
101-
`${import.meta.env.VITE_BACKEND_SERVER}/unsaveOpportunity/${opportunity.id}`, {
102-
method: "DELETE",
103-
credentials: "include",
104-
headers,
105-
});
106-
107-
if (!response.ok) {
108-
throw new Error("Failed to unsave");
109-
}
110-
111-
setSaved(prev => prev ? prev.filter(o => o.id !== opportunity.id) : prev);
112-
} catch {
113-
console.log("Error unsaving opportunity");
114-
}
115-
}}
116-
>
117-
Unsave
118-
</button>
119-
</td>
172+
);
173+
174+
if (!response.ok) {
175+
throw new Error("Failed to unsave");
176+
}
177+
178+
setSaved((prev) =>
179+
prev
180+
? prev.filter(
181+
(o) => o.id !== opportunity.id
182+
)
183+
: prev
184+
);
185+
} catch {
186+
console.log(
187+
"Error unsaving opportunity"
188+
);
189+
}
190+
}}
191+
>
192+
Unsave
193+
</button>
194+
</td>
120195
</tr>
121-
))}
196+
);
197+
})}
198+
</tbody>
122199
</table>
200+
</div>
123201
)}
124-
</section>
125-
);
126-
};
202+
</div>
203+
)}
204+
</div>
205+
</section>
206+
);
207+
}

src/opportunities/components/FiltersField.tsx

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,42 +13,57 @@ interface FiltersFieldProps {
1313
setPopUpMenu: () => void;
1414
}
1515

16-
export default function FiltersField({ resetFilters, deleteFilter, filters, setPopUpMenu }: FiltersFieldProps) {
16+
export default function FiltersField({
17+
resetFilters,
18+
deleteFilter,
19+
filters,
20+
setPopUpMenu,
21+
}: FiltersFieldProps) {
1722
return (
18-
<div>
19-
<hr />
23+
<div className="text-gray-800 dark:text-gray-200">
24+
<hr className="border-gray-300 dark:border-gray-700" />
25+
2026
<div className="px-3 max-h-20 flex">
2127
<div className="flex gap-2 w-full">
28+
{/* Make sure SearchBar forwards className to the actual <input> */}
2229
<SearchBar />
2330

24-
<SmallTextButton className="" onClick={setPopUpMenu} special={true}>
31+
<SmallTextButton
32+
33+
onClick={setPopUpMenu}
34+
special={true}
35+
>
2536
<PiSlidersHorizontal className="pr-1" />
2637
Change Filters
2738
<PiSlidersHorizontal className="pl-1" />
2839
</SmallTextButton>
2940

30-
{/* Fix rendering with new filters = [ [],[],[] ]*/}
41+
{/* Filter “chips” */}
3142
<GroupedComponents gap={2}>
32-
{filters.map((filter) => {
33-
return (
34-
<HorizontalIconButton
35-
onClick={deleteFilter}
36-
icon={<MdCancel />}
37-
key={filter}
38-
special={false}
39-
>
40-
{filter}
41-
</HorizontalIconButton>
42-
)
43-
})}
43+
{filters.map((filter) => (
44+
<HorizontalIconButton
45+
key={filter}
46+
onClick={deleteFilter}
47+
icon={<MdCancel />}
48+
special={false}
49+
50+
>
51+
{filter}
52+
</HorizontalIconButton>
53+
))}
4454
</GroupedComponents>
4555
</div>
4656

47-
<SmallTextButton className="flex flex-right" onClick={resetFilters} special={true}>
57+
<SmallTextButton
58+
onClick={resetFilters}
59+
special={true}
60+
>
4861
Reset
4962
</SmallTextButton>
5063
</div>
51-
<hr />
64+
65+
<hr className="border-gray-300 dark:border-gray-700" />
5266
</div>
5367
);
54-
};
68+
}
69+

0 commit comments

Comments
 (0)