Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
297 changes: 189 additions & 108 deletions src/individuals/pages/Saved.tsx
Original file line number Diff line number Diff line change
@@ -1,126 +1,207 @@
import React, { useEffect } from "react";
import { useState } from "react";
import React, { useEffect, useState } from "react";
import { useAuth } from "../../context/AuthContext.tsx";
import { Opportunity } from "../../types/opportunity.ts";
import { getCookie } from "../../utils.ts";

export default function SavedPage() {
const { auth } = useAuth();
const { auth } = useAuth();

if (!auth.isAuthenticated) {
window.location.href = "/login";
}
if (!auth.isAuthenticated) {
window.location.href = "/login";
}

const [saved, setSaved] = useState<null | Opportunity[]>(null);
const [saved, setSaved] = useState<Opportunity[] | null>(null);

const csrfToken = getCookie('csrf_access_token');
const csrfToken = getCookie("csrf_access_token");

const fetchSaved = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_BACKEND_SERVER}/savedOpportunities`, {
credentials: "include",
}
);
const fetchSaved = async () => {
try {
const response = await fetch(
`${import.meta.env.VITE_BACKEND_SERVER}/savedOpportunities`,
{
credentials: "include",
}
);

if (!response.ok) {
throw new Error("Saved not found");
}
if (!response.ok) {
throw new Error("Saved not found");
}

const data = await response.json();
setSaved(data);
console.log(data);
} catch {
console.log("Error fetching saved");
}
const data = await response.json();
setSaved(data);
} catch {
console.log("Error fetching saved");
}
};

useEffect(() => {
fetchSaved();
}, []);

return (
<section className="w-full px-4 py-8 md:px-8 lg:px-10">
<div className="max-w-6xl mx-auto">
<h1 className="text-center my-4 text-3xl md:text-4xl font-extrabold tracking-tight text-gray-900 dark:text-gray-100">
Saved Opportunities
</h1>

{saved === null && (
<p className="mt-6 text-center text-gray-500 dark:text-gray-400">
Loading...
</p>
)}

useEffect(() => {
fetchSaved();
}, []);

return (
<section className="center container-xl">
<h1 className="text-center my-4 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl">
Saved Opportunities
</h1>
{!saved && "Loading..."}
{saved && (
<table>
<tr>
<th>Name</th>
<th>Description</th>
<th>Recommended Experience</th>
<th>Pay</th>
<th>Credits</th>
<th>Semester</th>
<th>Year</th>
<th>Application Due</th>
<th>Location</th>
<th>Unsave</th>
{saved !== null && (
<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">
{saved.length === 0 ? (
<p className="text-center text-gray-500 dark:text-gray-400 py-4">
You don’t have any saved opportunities yet.
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse text-sm md:text-base">
<thead className="top-0 z-10">
<tr className="bg-gray-100 dark:bg-gray-800/80 backdrop-blur">
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Name
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Description
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Recommended Experience
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Pay
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Credits
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Semester
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Year
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Application Due
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Location
</th>
<th className="p-3 text-left border border-gray-300 dark:border-gray-600">
Unsave
</th>
</tr>
{saved.map((opportunity) => (
<tr key={opportunity.id}>
<td>{opportunity.name}</td>
<td>{opportunity.description}</td>
<td>{opportunity.recommended_experience}</td>
<td>{opportunity.pay}</td>
<td>{opportunity.credits}</td>
<td>{opportunity.semester}</td>
<td>{opportunity.year}</td>
<td style={{
color: (() => {
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";
</thead>

<tbody className="[&_tr:nth-child(even)]:bg-transparent dark:[&_tr:nth-child(even)]:bg-gray-800/30">
{saved.map((opportunity) => {
const today = new Date();
const dueDate = new Date(opportunity.application_due);
const oneWeek = 7 * 24 * 60 * 60 * 1000;

let dueClass =
"p-3 border border-gray-300 dark:border-gray-600";
if (dueDate < today) {
dueClass += " text-red-500 font-semibold";
} else if (
dueDate.getTime() - today.getTime() <= oneWeek
) {
dueClass += " text-orange-400 font-semibold";
}

return (
<tr
key={opportunity.id}
className="hover:bg-gray-50 dark:hover:bg-gray-800/60 transition-colors"
>
<td className="p-3 border border-gray-300 dark:border-gray-600 font-medium">
{opportunity.name}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
{opportunity.description}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
{opportunity.recommended_experience}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
{opportunity.pay
? `$${opportunity.pay}/hr`
: ""}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
{opportunity.credits}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
{opportunity.semester}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
{opportunity.year}
</td>
<td className={dueClass}>
{new Date(
opportunity.application_due
).toLocaleDateString("en-US")}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
{opportunity.location}
</td>
<td className="p-3 border border-gray-300 dark:border-gray-600">
<button
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"
onClick={async () => {
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (csrfToken) {
headers["X-CSRF-TOKEN"] = csrfToken;
}

const response = await fetch(
`${import.meta.env
.VITE_BACKEND_SERVER}/unsaveOpportunity/${
opportunity.id
}`,
{
method: "DELETE",
credentials: "include",
headers,
}
})()
}}>
{new Date(opportunity.application_due).toLocaleDateString("en-US")}
</td>
<td>{opportunity.location}</td>
<td>
<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"
onClick={async () => {
try {
const headers: Record<string, string> = {
"Content-Type": "application/json", // Good practice for cross-origin requests
};
if (csrfToken) {
headers["X-CSRF-TOKEN"] = csrfToken; // Include the token only when defined
}

const response = await fetch(
`${import.meta.env.VITE_BACKEND_SERVER}/unsaveOpportunity/${opportunity.id}`, {
method: "DELETE",
credentials: "include",
headers,
});

if (!response.ok) {
throw new Error("Failed to unsave");
}

setSaved(prev => prev ? prev.filter(o => o.id !== opportunity.id) : prev);
} catch {
console.log("Error unsaving opportunity");
}
}}
>
Unsave
</button>
</td>
);

if (!response.ok) {
throw new Error("Failed to unsave");
}

setSaved((prev) =>
prev
? prev.filter(
(o) => o.id !== opportunity.id
)
: prev
);
} catch {
console.log(
"Error unsaving opportunity"
);
}
}}
>
Unsave
</button>
</td>
</tr>
))}
);
})}
</tbody>
</table>
</div>
)}
</section>
);
};
</div>
)}
</div>
</section>
);
}
55 changes: 35 additions & 20 deletions src/opportunities/components/FiltersField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,57 @@ interface FiltersFieldProps {
setPopUpMenu: () => void;
}

export default function FiltersField({ resetFilters, deleteFilter, filters, setPopUpMenu }: FiltersFieldProps) {
export default function FiltersField({
resetFilters,
deleteFilter,
filters,
setPopUpMenu,
}: FiltersFieldProps) {
return (
<div>
<hr />
<div className="text-gray-800 dark:text-gray-200">
<hr className="border-gray-300 dark:border-gray-700" />

<div className="px-3 max-h-20 flex">
<div className="flex gap-2 w-full">
{/* Make sure SearchBar forwards className to the actual <input> */}
<SearchBar />

<SmallTextButton className="" onClick={setPopUpMenu} special={true}>
<SmallTextButton

onClick={setPopUpMenu}
special={true}
>
<PiSlidersHorizontal className="pr-1" />
Change Filters
<PiSlidersHorizontal className="pl-1" />
</SmallTextButton>

{/* Fix rendering with new filters = [ [],[],[] ]*/}
{/* Filter “chips” */}
<GroupedComponents gap={2}>
{filters.map((filter) => {
return (
<HorizontalIconButton
onClick={deleteFilter}
icon={<MdCancel />}
key={filter}
special={false}
>
{filter}
</HorizontalIconButton>
)
})}
{filters.map((filter) => (
<HorizontalIconButton
key={filter}
onClick={deleteFilter}
icon={<MdCancel />}
special={false}

>
{filter}
</HorizontalIconButton>
))}
</GroupedComponents>
</div>

<SmallTextButton className="flex flex-right" onClick={resetFilters} special={true}>
<SmallTextButton
onClick={resetFilters}
special={true}
>
Reset
</SmallTextButton>
</div>
<hr />

<hr className="border-gray-300 dark:border-gray-700" />
</div>
);
};
}

Loading