Skip to content

Commit 129f18a

Browse files
Create page to show saved opportunities (#165)
Saved opportunities page Reminder for me: change the routes to vite combine / get rid of duplicate saved section on Oppotunities page
1 parent 32c570b commit 129f18a

File tree

8 files changed

+172
-21
lines changed

8 files changed

+172
-21
lines changed

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/App.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Home from "./shared/pages/Home.tsx";
55
import PageNotFound from "./shared/pages/404.tsx";
66
import MainNavigation from "./shared/components/Navigation/MainNavigation.tsx";
77
import StickyFooter from "./shared/components/Navigation/StickyFooter.tsx";
8-
import ProfilePage from "./shared/pages/Profile.tsx";
8+
import ProfilePage from "./individuals/pages/Profile.tsx";
99
import Departments from "./staff/pages/Departments.tsx";
1010
import StaffPage from "./staff/pages/Staff.tsx";
1111
import Department from "./staff/pages/Department.tsx";
@@ -15,6 +15,7 @@ import LogoutRedirection from "./auth/Logout.tsx";
1515
import Token from "./auth/Token.tsx";
1616
import Opportunities from "./opportunities/pages/Opportunities.tsx";
1717
import IndividualPost from "./opportunities/pages/IndividualPost.tsx";
18+
import SavedPage from "./individuals/pages/Saved.tsx";
1819
import { HelmetProvider } from 'react-helmet-async';
1920
import { AuthProvider } from './context/AuthContext.tsx';
2021

@@ -35,7 +36,11 @@ function App() {
3536
<Route path="/logout" element={<LogoutRedirection />} />
3637
<Route path="/opportunities" element={<Opportunities />} />
3738
<Route path="/profile" element={<ProfilePage />} />
38-
<Route path="/staff/department/:department" element={<Department />} />
39+
<Route path="/saved" element={<SavedPage />} />
40+
<Route
41+
path="/staff/department/:department"
42+
element={<Department />}
43+
/>
3944
<Route path="/staff" element={<Departments />} />
4045
<Route path="/staff/:staffId" element={<StaffPage />} />
4146
<Route path="/create" element={<CreatePost edit={false} />} />
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useEffect } from "react";
22
import { useState } from "react";
3-
import ProfileComponents from "../components/Profile/ProfileComponents.tsx";
3+
import ProfileComponents from "../../shared/components/Profile/ProfileComponents.tsx";
44
import { useAuth } from "../../context/AuthContext.tsx";
55
import { Profile } from "../../types/profile.ts";
66
// import EditProfile from "./EditProfile";

src/individuals/pages/Saved.tsx

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import React, { useEffect } from "react";
2+
import { useState } from "react";
3+
import { useAuth } from "../../context/AuthContext.tsx";
4+
import { Opportunity } from "../../types/opportunity.ts";
5+
import { getCookie } from "../../utils.ts";
6+
7+
export default function SavedPage() {
8+
const { auth } = useAuth();
9+
10+
if (!auth.isAuthenticated) {
11+
window.location.href = "/login";
12+
}
13+
14+
const [saved, setSaved] = useState<null | Opportunity[]>(null);
15+
16+
const csrfToken = getCookie('csrf_access_token');
17+
18+
const fetchSaved = async () => {
19+
try {
20+
const response = await fetch(
21+
`${process.env.REACT_APP_BACKEND_SERVER}/savedOpportunities`, {
22+
credentials: "include",
23+
}
24+
);
25+
26+
if (!response.ok) {
27+
throw new Error("Saved not found");
28+
}
29+
30+
const data = await response.json();
31+
setSaved(data);
32+
console.log(data);
33+
} catch {
34+
console.log("Error fetching saved");
35+
}
36+
}
37+
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>
61+
</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";
83+
}
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+
`${process.env.REACT_APP_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>
120+
</tr>
121+
))}
122+
</table>
123+
)}
124+
</section>
125+
);
126+
};

src/shared/components/Navigation/MainNavigation.tsx

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ export default function MainNavigation() {
2121
// Define navigation routes based on authentication.
2222
const routes = auth.isAuthenticated
2323
? [
24-
{ name: "Opportunities", href: "/opportunities" },
25-
{ name: "Create", href: "/create" },
26-
{ name: "Staff", href: "/staff" },
27-
{ name: "Profile", href: "/profile" },
28-
{ name: "Sign Out", href: "/signout" }
29-
]
24+
{ name: "Opportunities", href: "/opportunities" },
25+
{ name: "Create", href: "/create" },
26+
{ name: "Staff", href: "/staff" },
27+
{ name: "Profile", href: "/profile" },
28+
{ name: "Saved", href: "/saved" },
29+
{ name: "Sign Out", href: "/signout" }
30+
]
3031
: [{ name: "Sign In", href: "/signin" }];
3132

3233
return (
@@ -53,8 +54,7 @@ export default function MainNavigation() {
5354
key={item.name}
5455
to={item.href}
5556
className={({ isActive }) =>
56-
`text-2xl font-bold hover:underline ${
57-
isActive ? "underline" : ""
57+
`text-2xl font-bold hover:underline ${isActive ? "underline" : ""
5858
}`
5959
}
6060
>
@@ -75,9 +75,8 @@ export default function MainNavigation() {
7575
/>
7676
<div className="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full" />
7777
<div
78-
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${
79-
isDarkMode ? "translate-x-5" : ""
80-
}`}
78+
className={`absolute left-1 top-1 bg-white w-4 h-4 rounded-full transition-transform ${isDarkMode ? "translate-x-5" : ""
79+
}`}
8180
></div>
8281
</div>
8382
<span className="ml-3 text-white text-lg font-bold">
@@ -118,9 +117,8 @@ export default function MainNavigation() {
118117
/>
119118
<div className="w-11 h-6 bg-gray-300 dark:bg-gray-600 rounded-full peer-focus:outline-none peer-checked:bg-blue-600 relative transition-all duration-300">
120119
<div
121-
className={`absolute left-1 top-1 bg-white dark:bg-gray-300 w-4 h-4 rounded-full transition-transform duration-300 ${
122-
isDarkMode ? "translate-x-5" : ""
123-
}`}
120+
className={`absolute left-1 top-1 bg-white dark:bg-gray-300 w-4 h-4 rounded-full transition-transform duration-300 ${isDarkMode ? "translate-x-5" : ""
121+
}`}
124122
></div>
125123
</div>
126124
<span className="ml-3 text-gray-700 dark:text-gray-200 font-medium">

src/staff/pages/Departments.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export default function Departments() {
4646
return (
4747
<>
4848
<SEO title="Departments - Labconnect" description="Labconnect departments page" />
49-
<h1 className="mb-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl">
49+
<h1 className="text-center my-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl">
5050
Departments
5151
</h1>
5252
{!departments && "Loading..."}

src/types/opportunity.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export type Opportunity = {
2+
id: string;
3+
name: string;
4+
description: string;
5+
recommended_experience?: string;
6+
pay?: number;
7+
credits?: string;
8+
semester: string;
9+
year: number;
10+
application_due: string;
11+
active: boolean;
12+
location: string;
13+
}

src/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function getCookie(name: string) {
2+
const value = `; ${document.cookie}`;
3+
const parts = value.split(`; ${name}=`);
4+
if (parts.length === 2) {
5+
const part = parts[1];
6+
return part.split(';')[0];
7+
}
8+
return undefined;
9+
}

0 commit comments

Comments
 (0)