From 34144e0c44d73347a117f526b48c55a1c96922da Mon Sep 17 00:00:00 2001 From: doan-neyugn Date: Tue, 21 Oct 2025 18:07:14 -0400 Subject: [PATCH 1/4] Dark mode for opportunities and create pages --- package-lock.json | 6 +- src/opportunities/components/FiltersField.tsx | 55 +++++++----- .../components/OpportunitiesDetails.tsx | 83 ++++++++++++------- src/opportunities/pages/Opportunities.tsx | 51 +++++++----- src/staff/pages/CreatePost.tsx | 25 ++++-- 5 files changed, 139 insertions(+), 81 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8c91b75..13d6c3d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6927,9 +6927,9 @@ } }, "node_modules/vite": { - "version": "6.3.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", - "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/opportunities/components/FiltersField.tsx b/src/opportunities/components/FiltersField.tsx index cf049855..458d3e4f 100644 --- a/src/opportunities/components/FiltersField.tsx +++ b/src/opportunities/components/FiltersField.tsx @@ -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 ( -
-
+
+
+
+ {/* Make sure SearchBar forwards className to the actual */} - + Change Filters - {/* Fix rendering with new filters = [ [],[],[] ]*/} + {/* Filter “chips” */} - {filters.map((filter) => { - return ( - } - key={filter} - special={false} - > - {filter} - - ) - })} + {filters.map((filter) => ( + } + special={false} + + > + {filter} + + ))}
- + Reset
-
+ +
); -}; +} + diff --git a/src/opportunities/components/OpportunitiesDetails.tsx b/src/opportunities/components/OpportunitiesDetails.tsx index a1f98282..101c4c76 100644 --- a/src/opportunities/components/OpportunitiesDetails.tsx +++ b/src/opportunities/components/OpportunitiesDetails.tsx @@ -7,44 +7,66 @@ interface OpportunitiesListProps { export default function OpportunitiesList({ opportunities }: OpportunitiesListProps) { return ( -
+
- - {/* Column Headers */} - - - - - - - - - - + + + + + + + + + + + - - {/* Info about the opportunities */} + + {opportunities.length > 0 ? ( opportunities.map((opportunity) => ( - - - - - - - - + + + + + + + - - @@ -52,7 +74,10 @@ export default function OpportunitiesList({ opportunities }: OpportunitiesListPr )) ) : ( - @@ -62,4 +87,4 @@ export default function OpportunitiesList({ opportunities }: OpportunitiesListPr ); -}; \ No newline at end of file +} diff --git a/src/opportunities/pages/Opportunities.tsx b/src/opportunities/pages/Opportunities.tsx index f862ec3e..c82a313b 100644 --- a/src/opportunities/pages/Opportunities.tsx +++ b/src/opportunities/pages/Opportunities.tsx @@ -8,43 +8,48 @@ interface PageNavigationType { } const Opportunities: React.FC = () => { - // navigation bar const [pages, switchPage] = usePageNavigation(["Search", "Saved"], "Search") as [ PageNavigationType, (page: string) => void ]; - const activeLink = "text-black py-3 border-b-2 border-black text-lg"; - const normalLink = "text-gray-600 py-3 text-lg border-black hover:border-b-2 hover:text-black"; + // Tailwind classes with dark variants + const activeLink = + "py-3 text-lg font-semibold border-b-2 " + + "text-black dark:text-gray-100 " + + "border-black dark:border-gray-100"; + + const normalLink = + "py-3 text-lg font-semibold border-b-2 border-transparent " + + "text-gray-600 dark:text-gray-300 " + + "hover:text-black dark:hover:text-white " + + "hover:border-black dark:hover:border-gray-100"; - // displaying opportunities list component return ( -
+
-
-

Opportunities

- -
diff --git a/src/staff/pages/CreatePost.tsx b/src/staff/pages/CreatePost.tsx index 0fad0bc5..c85d977b 100644 --- a/src/staff/pages/CreatePost.tsx +++ b/src/staff/pages/CreatePost.tsx @@ -12,13 +12,26 @@ export default function CreatePost({ edit }: CreatePostProps) { if (!auth.isAuthenticated) { window.location.href = "/login"; + return null; } return ( -
- -

{edit === true ? "Edit Research Opportunity" : "Create Research Opportunity"}

- -
+
+ + +
+

+ {edit ? "Edit Research Opportunity" : "Create Research Opportunity"} +

+ + {/* subtle card so fields pop in dark mode */} +
+ +
+
+
); -}; +} From af9ce6d5a7872f82201d90ae8b53e0c02690754b Mon Sep 17 00:00:00 2001 From: doan-neyugn Date: Fri, 24 Oct 2025 02:51:53 -0400 Subject: [PATCH 2/4] dark mode for other pages --- .../components/OpportunitiesDetails.tsx | 2 +- src/opportunities/pages/IndividualPost.tsx | 42 ++++++++++---- .../components/Profile/ProfileDescription.tsx | 34 ++++++++--- .../Profile/ProfileOpportunities.tsx | 57 +++++++++++++++---- src/shared/pages/Home.tsx | 2 +- 5 files changed, 104 insertions(+), 33 deletions(-) diff --git a/src/opportunities/components/OpportunitiesDetails.tsx b/src/opportunities/components/OpportunitiesDetails.tsx index 2ae5c0d9..f9b352bd 100644 --- a/src/opportunities/components/OpportunitiesDetails.tsx +++ b/src/opportunities/components/OpportunitiesDetails.tsx @@ -11,7 +11,7 @@ export default function OpportunitiesList({ opportunities }: OpportunitiesListPr
PositionDescriptionLocationPayCreditsLab ManagersTermViewSave
PositionDescriptionLocationPayCreditsLab ManagersTermViewSave
{opportunity.name}{opportunity.description}{opportunity.location}{opportunity.pay ? `$${opportunity.pay}/hr` : ""}{opportunity.credits}{opportunity.lab_managers} +
+ {opportunity.name} + + {opportunity.description} + + {opportunity.location} + + {opportunity.pay ? `$${opportunity.pay}/hr` : ""} + + {opportunity.credits} + + {opportunity.lab_managers} + {opportunity.semester} {opportunity.year} - + - +
+ No results found.
- + diff --git a/src/opportunities/pages/IndividualPost.tsx b/src/opportunities/pages/IndividualPost.tsx index 75ed8a26..1a7ffae4 100644 --- a/src/opportunities/pages/IndividualPost.tsx +++ b/src/opportunities/pages/IndividualPost.tsx @@ -1,7 +1,6 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import JobDetails from "../components/JobDetails"; import { useParams } from "react-router-dom"; -import { useEffect } from "react"; const IndividualPost = () => { const { postID } = useParams(); @@ -9,7 +8,6 @@ const IndividualPost = () => { const [details, setDetails] = useState("Searching"); const fetchOpportunities = async () => { - // Consider moving the base URL to a configuration const baseURL = `${import.meta.env.VITE_BACKEND_SERVER}`; const url = `${baseURL}/getOpportunity/${postID}`; @@ -32,21 +30,43 @@ const IndividualPost = () => { findDetails(); }); - - - - return ( -
+
{details === "Searching" ? ( - +
+ +

+ Loading opportunity details… +

+
) : details === "Nothing found" ? ( -

No post found

+
+

No post found

+
) : ( - +
+ +
)}
); }; export default IndividualPost; + diff --git a/src/shared/components/Profile/ProfileDescription.tsx b/src/shared/components/Profile/ProfileDescription.tsx index 7e510529..b60003b5 100644 --- a/src/shared/components/Profile/ProfileDescription.tsx +++ b/src/shared/components/Profile/ProfileDescription.tsx @@ -3,19 +3,37 @@ import { Link } from "react-router-dom"; import { Profile } from "../../../types/profile.ts"; export default function ProfileDescription({ - name, department, description, website, pronouns + name, + department, + description, + website, + pronouns, }: Profile) { return ( -
+

{name}

-
{department}
- {pronouns &&
{pronouns}
} -

{description}

- {website && website.length && ( - +
{department}
+ {pronouns &&
{pronouns}
} +

{description}

+ {website && website.length > 0 && ( + {website} )}
); -}; \ No newline at end of file +} diff --git a/src/shared/components/Profile/ProfileOpportunities.tsx b/src/shared/components/Profile/ProfileOpportunities.tsx index 37e6d097..c13f9b7a 100644 --- a/src/shared/components/Profile/ProfileOpportunities.tsx +++ b/src/shared/components/Profile/ProfileOpportunities.tsx @@ -2,15 +2,22 @@ import React from "react"; import LargeTextCard from "../UIElements/LargeTextCard.tsx"; import { useState, useEffect } from "react"; -export default function ProfileOpportunities({ id, staff }: { id: string, staff: boolean }) { - const [opportunities, setOpportunities] = useState | null | "no response">(null); +export default function ProfileOpportunities({ + id, + staff, +}: { + id: string; + staff: boolean; +}) { + const [opportunities, setOpportunities] = useState< + Array<{ id: string; title: string; due: string; pay: string; credits: string }> | null | "no response" + >(null); useEffect(() => { async function setData() { const response = await fetch( - `${import.meta.env.VITE_BACKEND_SERVER}/${staff ? "staff" : "profile"}/opportunities/${id}`, { - credentials: "include", - } + `${import.meta.env.VITE_BACKEND_SERVER}/${staff ? "staff" : "profile"}/opportunities/${id}`, + { credentials: "include" } ); if (response.ok) { @@ -25,11 +32,24 @@ export default function ProfileOpportunities({ id, staff }: { id: string, staff: }, [id, staff]); const opportunityList = ( -
+
{id && Array.isArray(opportunities) && opportunities.map((opportunity) => ( - -

Posted Opportunities:

- {opportunities !== null ? opportunityList : "Loading..."} - {opportunities === "no response" && "No Opportunities Found"} +
+

+ Posted Opportunities: +

+ + {/* Loaded list */} + {opportunities !== null && opportunities !== "no response" && opportunityList} + + {/* Loading / empty states */} + {opportunities === null && ( +

Loading...

+ )} + {opportunities === "no response" && ( +

+ No Opportunities Found +

+ )}
); -}; +} diff --git a/src/shared/pages/Home.tsx b/src/shared/pages/Home.tsx index 7d108ba2..df7267f7 100644 --- a/src/shared/pages/Home.tsx +++ b/src/shared/pages/Home.tsx @@ -281,7 +281,7 @@ const Home = () => { { name: "Doan Nguyen", major: "Computer Science", - gradYear: "2027", + gradYear: "2026", role: "Frontend Developer", } ].map((member, index) => ( From 9c3620275d647e3deee4fb541796e38b594e7596 Mon Sep 17 00:00:00 2001 From: doan-neyugn <145173728+nguyed8@users.noreply.github.com> Date: Wed, 3 Dec 2025 20:35:24 -0500 Subject: [PATCH 3/4] dark mode for saved, popup menu, and job details --- src/individuals/pages/Saved.tsx | 297 +++++++++++------- src/opportunities/components/JobDetails.tsx | 57 ++-- .../components/OpportunitiesDetails.tsx | 2 +- src/opportunities/components/PopUpMenu.tsx | 104 ++++-- 4 files changed, 305 insertions(+), 155 deletions(-) diff --git a/src/individuals/pages/Saved.tsx b/src/individuals/pages/Saved.tsx index 4a5ecb69..9a2f662b 100644 --- a/src/individuals/pages/Saved.tsx +++ b/src/individuals/pages/Saved.tsx @@ -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); + const [saved, setSaved] = useState(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 ( +
+
+

+ Saved Opportunities +

+ + {saved === null && ( +

+ Loading... +

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

- Saved Opportunities -

- {!saved && "Loading..."} - {saved && ( -
Position Description
- - - - - - - - - - - + {saved !== null && ( +
+ {saved.length === 0 ? ( +

+ You don’t have any saved opportunities yet. +

+ ) : ( +
+
NameDescriptionRecommended ExperiencePayCreditsSemesterYearApplication DueLocationUnsave
+ + + + + + + + + + + + - {saved.map((opportunity) => ( - - - - - - - - - + + + {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 ( + + + + + + + + + + + - - + ); + + 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 + + - ))} + ); + })} +
+ Name + + Description + + Recommended Experience + + Pay + + Credits + + Semester + + Year + + Application Due + + Location + + Unsave +
{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"; +
+ {opportunity.name} + + {opportunity.description} + + {opportunity.recommended_experience} + + {opportunity.pay + ? `$${opportunity.pay}/hr` + : ""} + + {opportunity.credits} + + {opportunity.semester} + + {opportunity.year} + + {new Date( + opportunity.application_due + ).toLocaleDateString("en-US")} + + {opportunity.location} + + {opportunity.location} - -
+
)} - - ); -}; +
+ )} +
+ + ); +} diff --git a/src/opportunities/components/JobDetails.tsx b/src/opportunities/components/JobDetails.tsx index f898189c..0b179c28 100644 --- a/src/opportunities/components/JobDetails.tsx +++ b/src/opportunities/components/JobDetails.tsx @@ -19,25 +19,46 @@ const JobDetails = ({ recommended_experience, }: JobDetailsProps) => { return ( -
- - +
+
+
+ {/* Accent vertical line */} +
- -
+
+ + + + + +
+
+ + ); }; diff --git a/src/opportunities/components/OpportunitiesDetails.tsx b/src/opportunities/components/OpportunitiesDetails.tsx index f9b352bd..1d2fde98 100644 --- a/src/opportunities/components/OpportunitiesDetails.tsx +++ b/src/opportunities/components/OpportunitiesDetails.tsx @@ -53,7 +53,7 @@ export default function OpportunitiesList({ opportunities }: OpportunitiesListPr {opportunity.semester} {opportunity.year} - + + + {/* Submit Button */} + + + + + - ) : loading === "no response" ? ( -

There was no response

- ) : ( -

Loading...

); -}; \ No newline at end of file +} diff --git a/src/staff/pages/CreatePost.tsx b/src/staff/pages/CreatePost.tsx index c85d977b..5db1995d 100644 --- a/src/staff/pages/CreatePost.tsx +++ b/src/staff/pages/CreatePost.tsx @@ -16,19 +16,27 @@ export default function CreatePost({ edit }: CreatePostProps) { } return ( -
+
-
-

+
+

{edit ? "Edit Research Opportunity" : "Create Research Opportunity"}

- {/* subtle card so fields pop in dark mode */} -
+ {/* Single clear card, same style as other pages */} +