diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index d1dc50a3..a2e7a67d 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -23,6 +23,6 @@ jobs: - name: Run EsLint uses: sibiraj-s/action-eslint@v3 with: - eslint-args: "src/**/*.{js,jsx,ts,tsx} --max-warnings=0" - extensions: "js,jsx,ts,tsx" + eslint-args: "src/**/*.{ts,tsx} --max-warnings=0" + extensions: "ts,tsx" annotations: true diff --git a/src/App.tsx b/src/App.tsx index c0f0f5e2..af5e2747 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import StaffPage from "./staff/pages/Staff.tsx"; import Department from "./staff/pages/Department.tsx"; import CreatePost from "./staff/pages/CreatePost.tsx"; import IndividualPost from "./opportunities/pages/IndividualPost.js"; -import ProfilePage from "./shared/pages/Profile.js"; +import ProfilePage from "./shared/pages/Profile.tsx"; import LoginRedirection from "./auth/Login.tsx"; import LogoutRedirection from "./auth/Logout.tsx"; import StickyFooter from "./shared/components/Navigation/StickyFooter.tsx"; diff --git a/src/auth/Logout.tsx b/src/auth/Logout.tsx index 1d31e828..08b71716 100644 --- a/src/auth/Logout.tsx +++ b/src/auth/Logout.tsx @@ -32,7 +32,7 @@ export default function LogoutRedirection() { } } logoutUser(); - }, [auth, logout]); + }, [logout, auth.token]); return null; // Since this component doesn't need to render anything }; \ No newline at end of file diff --git a/src/auth/Token.tsx b/src/auth/Token.tsx index 515f5d87..411cf617 100644 --- a/src/auth/Token.tsx +++ b/src/auth/Token.tsx @@ -1,4 +1,5 @@ import { useAuth } from "../context/AuthContext.tsx"; +import { useEffect } from "react"; export default function Token() { @@ -7,26 +8,30 @@ export default function Token() { const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get("code"); - if (code) { - fetch(`${process.env.REACT_APP_BACKEND_SERVER}/token`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ code }), - }) - .then((response) => response.json()) - .then((data) => { - const token = data.token; - if (token) { - login(token); - return null; - } + useEffect(() => { + async function fetchToken(code: string) { + fetch(`${process.env.REACT_APP_BACKEND_SERVER}/token`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ code }), }) - .catch((error) => console.error("Error fetching token:", error)); - } else { - window.location.href = "/"; - } + .then((response) => response.json()) + .then((data) => { + const token = data.token; + if (token) { + login(token); + window.location.href = "/"; + return null; + } + }) + .catch((error) => console.error("Error fetching token:", error)); + } + if (code) { + fetchToken(code); + } + }, [code, login]); return null; } \ No newline at end of file diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index add455b5..ab76457f 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -1,4 +1,6 @@ -import React, { createContext, useState, useContext } from 'react'; +import React, { createContext, useState, useContext, useEffect } from 'react'; +import { ReactNode } from 'react'; + const AuthContext = createContext<{ auth: { isAuthenticated: boolean; token: string | null }; @@ -11,9 +13,6 @@ const AuthContext = createContext<{ logout: () => { }, loadToken: () => { } }); - -import { ReactNode } from 'react'; - interface AuthProviderProps { children: ReactNode; } @@ -43,6 +42,10 @@ export const AuthProvider = ({ children }: AuthProviderProps) => { } }; + useEffect(() => { + loadToken(); + }, []); + return ( {children} diff --git a/src/shared/components/Navigation/MainNavigation.tsx b/src/shared/components/Navigation/MainNavigation.tsx index 2bcb4042..62131eea 100644 --- a/src/shared/components/Navigation/MainNavigation.tsx +++ b/src/shared/components/Navigation/MainNavigation.tsx @@ -2,11 +2,14 @@ import React from "react"; import { Disclosure } from "@headlessui/react"; import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; import { Link, NavLink, useLocation } from "react-router-dom"; +import { useAuth } from "../../../context/AuthContext.tsx"; -export default function MainNavigation(authenticated) { +export default function MainNavigation() { + + const { auth } = useAuth(); const location = useLocation().pathname; - const routes = authenticated.authenticated[1] + const routes = auth.isAuthenticated ? [ { name: "Jobs", href: "/jobs", current: true }, { name: "Create", href: "/create", current: false }, diff --git a/src/shared/components/Navigation/StickyFooter.tsx b/src/shared/components/Navigation/StickyFooter.tsx index 976a7c12..196bb07d 100644 --- a/src/shared/components/Navigation/StickyFooter.tsx +++ b/src/shared/components/Navigation/StickyFooter.tsx @@ -1,10 +1,13 @@ import React from "react"; import { Link } from "react-router-dom"; import logo from "../../../images/LabConnect_Logo.webp"; +import { useAuth } from "../../../context/AuthContext.tsx"; -export default function StickyFooter(authenticated) { +export default function StickyFooter() { - const routes = authenticated.authenticated[1] + const { auth } = useAuth(); + + const routes = auth.isAuthenticated ? [ { name: "Jobs", href: "/jobs", current: true }, { name: "Create", href: "/create", current: false }, diff --git a/src/shared/components/UIElements/ProfileAvatar.tsx b/src/shared/components/Profile/ProfileAvatar.tsx similarity index 100% rename from src/shared/components/UIElements/ProfileAvatar.tsx rename to src/shared/components/Profile/ProfileAvatar.tsx diff --git a/src/shared/components/Profile/ProfileComponents.tsx b/src/shared/components/Profile/ProfileComponents.tsx new file mode 100644 index 00000000..d09c1e1a --- /dev/null +++ b/src/shared/components/Profile/ProfileComponents.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import ProfileAvatar from "./ProfileAvatar.tsx"; +import ProfileDescription from "./ProfileDescription.tsx"; +import ProfileOpportunities from "./ProfileOpportunities.tsx"; +import SEO from "..//SEO.tsx"; +import Breadcrumb from "../UIElements/Breadcrumb.tsx"; + +interface Profile { + name: string; + image: string; + department: string; + description: string; + website?: string; +} + +const ProfileComponents = ({ profile, id, staff }: { profile: Profile, id: string, staff: boolean }) => { + return ( + <> + + {staff && } +
+
+ + +
+ {id && } +
+ + ); +} + +export default ProfileComponents; diff --git a/src/staff/components/ProfileDescription.tsx b/src/shared/components/Profile/ProfileDescription.tsx similarity index 82% rename from src/staff/components/ProfileDescription.tsx rename to src/shared/components/Profile/ProfileDescription.tsx index 10fbe213..fa25d1ad 100644 --- a/src/staff/components/ProfileDescription.tsx +++ b/src/shared/components/Profile/ProfileDescription.tsx @@ -8,9 +8,11 @@ const ProfileDescription = ({ name, department, description, website }) => {

{name}

{department}

{description}

- - {website} - + {website && website.length && ( + + {website} + + )} ); }; diff --git a/src/shared/components/Profile/ProfileOpportunities.js b/src/shared/components/Profile/ProfileOpportunities.js deleted file mode 100644 index 1ff23373..00000000 --- a/src/shared/components/Profile/ProfileOpportunities.js +++ /dev/null @@ -1,167 +0,0 @@ -import React from "react"; -import OpportunityActionCard from "./OpportunityActionCard"; -import { useState, useEffect } from "react"; - -const DUMMY_DATA = { - d1: [ - { - title: "Software Intern", - body: "Posted February 8, 2024", - attributes: ["Remote", "Paid", "Credits"], - activeStatus: true, - id: "o1", - }, - // create dummy data for the opportunities - { - title: "Biology Intern", - body: "Due February 2, 2024", - attributes: ["Paid", "Credits"], - activeStatus: true, - id: "o2", - }, - { - title: "Physics Intern", - body: "Due February 6, 2024", - attributes: ["Remote", "Paid", "Credits"], - activeStatus: true, - id: "o3", - }, - { - title: "Chemistry Intern", - body: "Due February 15, 2023", - attributes: ["Remote", "Paid", "Credits"], - activeStatus: true, - id: "o4", - }, - { - title: - "Mathematics Intern For the Sciences and Engineering Mathematics Intern For the Sciences and Engineering", - body: "Due February 1, 2024", - attributes: ["Remote", "Paid", "Credits"], - activeStatus: true, - id: "o5", - }, - ], -}; - -const ProfileOpportunities = ({ id }) => { - var [opportunities, setOpportunities] = useState(false); - - const fetchOpportunities = async (key) => { - // Consider moving the base URL to a configuration - const baseURL = `${process.env.REACT_APP_BACKEND_SERVER}`; - const url = `${baseURL}/getProfileOpportunities/${key}`; - - const response = await fetch(url); - - if (!response.ok) { - return false; - } - - const data = await response.json(); - console.log(data); - return data["data"]; - }; - - async function setData(key) { - const response = await fetchOpportunities(key); - response && setOpportunities(response); - response || setOpportunities("no response"); - } - - async function changeOpportunityActiveStatus(opportunityId, activeStatus) { - // send a request to the backend to deactivate the opportunity - // if the request is successful, then deactivate the opportunity from the list - const url = `${process.env.REACT_APP_BACKEND_SERVER}/changeActiveStatus`; - console.log(opportunities); - - const jsonData = { - oppID: opportunityId, - setStatus: !activeStatus, - authToken: "authTokenHere", - }; - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(jsonData), - }); - - if (response.ok) { - const data = await response.json(); - - const newOpportunities = opportunities.map((opportunity) => - opportunity.id === opportunityId - ? { ...opportunity, activeStatus: data.activeStatus } // Spread operator for update - : opportunity, - ); - - setOpportunities(newOpportunities); - } - } - - async function deleteOpportunity(opportunityId) { - // send a request to the backend to delete the opportunity - // if the request is successful, then delete the opportunity from the list - - const url = `${process.env.REACT_APP_BACKEND_SERVER}/deleteOpportunity`; - - const jsonData = { id: opportunityId }; - - const response = await fetch(url, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(jsonData), - }); - - if (response) { - opportunities = opportunities.filter( - (opportunity) => opportunity.id !== opportunityId, - ); - } else { - alert("Failed to delete opportunity"); - } - - setOpportunities(opportunities); - console.log(opportunities); - } - - useEffect(() => { - setData(id); - }, []); - - var opportuntityList = ( -
-

Posted Opportunties

-
- {opportunities && - opportunities.map((opportunity) => ( - - ))} -
-
- ); - - return opportunities ? ( - opportuntityList - ) : opportunities === "no response" ? ( - "No opportunities found" - ) : ( - - ); -}; - -export default ProfileOpportunities; diff --git a/src/staff/components/ProfileOpportunities.tsx b/src/shared/components/Profile/ProfileOpportunities.tsx similarity index 51% rename from src/staff/components/ProfileOpportunities.tsx rename to src/shared/components/Profile/ProfileOpportunities.tsx index 054a5a67..cc659444 100644 --- a/src/staff/components/ProfileOpportunities.tsx +++ b/src/shared/components/Profile/ProfileOpportunities.tsx @@ -1,44 +1,37 @@ import React from "react"; -import LargeTextCard from "./LargeTextCard"; -import PropTypes from "prop-types"; +import LargeTextCard from "../UIElements/LargeTextCard.tsx"; import { useState, useEffect } from "react"; +import { useAuth } from "../../../context/AuthContext.tsx"; -const ProfileOpportunities = ({ id }) => { +export default function ProfileOpportunities({ id, staff }: { id: string, staff: boolean }) { + const { auth } = useAuth(); const [opportunities, setOpportunities] = useState | null | "no response">(null); - const fetchOpportunities = async () => { - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/staff/opportunities/${id}` - ); - - if (!response.ok) { - throw new Error( - `Network response was not ok - Status: ${response.status}` + useEffect(() => { + async function setData() { + const response = await fetch( + `${process.env.REACT_APP_BACKEND_SERVER}/${staff ? "staff" : "profile"}/opportunities/${id}`, { + headers: { + Authorization: `Bearer ${auth.token}`, + }, + } ); - } - - const data = await response.json(); - return data["data"]; - }; - async function setData() { - const response = await fetchOpportunities(); - if (response) { - setOpportunities(response); - } else { - setOpportunities("no response"); + if (response.ok) { + const data = await response.json(); + setOpportunities(data); + } else { + setOpportunities("no response"); + } } - } - useEffect(() => { setData(); - }, []); + }, [auth.token, id, staff]); const opportunityList = (
{id && - opportunities && - typeof opportunities === "object" && + Array.isArray(opportunities) && opportunities.map((opportunity) => ( {
); }; - -ProfileOpportunities.propTypes = { - id: PropTypes.string.isRequired, -}; - -export default ProfileOpportunities; diff --git a/src/staff/components/LargeTextCard.js b/src/shared/components/UIElements/LargeTextCard.tsx similarity index 87% rename from src/staff/components/LargeTextCard.js rename to src/shared/components/UIElements/LargeTextCard.tsx index d63d246e..df6158c9 100644 --- a/src/staff/components/LargeTextCard.js +++ b/src/shared/components/UIElements/LargeTextCard.tsx @@ -8,9 +8,8 @@ const LargeTextCard = ({ to, title, due, pay, credits }) => {

100 ? "text-sm" : "text-lg font-bold" - } p-0 m-0`} + className={`${title.length > 100 ? "text-sm" : "text-lg font-bold" + } p-0 m-0`} > {title}

diff --git a/src/shared/pages/Profile.js b/src/shared/pages/Profile.js deleted file mode 100644 index d9e3374c..00000000 --- a/src/shared/pages/Profile.js +++ /dev/null @@ -1,125 +0,0 @@ -import React, { useEffect } from "react"; -import { useState } from "react"; -import ProfileAvatar from "../components/UIElements/ProfileAvatar.tsx"; -import ProfileDescription from "../../staff/components/ProfileDescription.tsx"; -import ProfileOpportunities from "../../staff/components/ProfileOpportunities.tsx"; -import EditProfile from "./EditProfile"; -import useGlobalContext from "../../context/global/useGlobalContext"; - -const PROFILES = { - d1: { - name: "Peter Johnson", - image: "https://www.bu.edu/com/files/2015/08/Katz-James-3.jpg", - researchCenter: "Computational Fake Center", - department: "Computer Science", - email: "johnp@rpi.edu", - role: "admin", - description: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut - pharetra sit amet aliquam id diam maecenas ultricies mi. Montes - nascetur ridiculus mus mauris vitae ultricies leo. Porttitor massa - id neque aliquam. Malesuada bibendum arcu vitae elementum. Nulla - aliquet porrsus mattis molestie aiaculis at erat pellentesque. - At risus viverra adipiscing at. - Tincidunt tortor aliquam nulla facilisi cras fermentum odio eu - feugiat. Eget fUt eu sem integer vitae justo - eget magna fermentum. Lobortis feugiat vivamus at augue eget arcu - dictum. Et tortor at risus viverra adipiscing at in tellus. - Suspendisse sed nisi lacus sed viverra tellus. Potenti nullam ac - tortor vitae. Massa id neque aliquam vestibulum. Ornare arcu odio ut - sem nulla pharetra. Quam id leo in vitae turpis massa. Interdum - velit euismod in pellentesque massa placerat duis ultricies lacus. - Maecenas sed enim ut sem viverra aliquet eget sit amet. Amet - venenatis urna cursus eget nunc scelerisque viverra mauris. Interdum - varius sit amet mattis. Aliquet nec ullamcorper sit amet risus - nullam. Aliquam faucibus purus in massa tempor nec feugiat. Vitae - turpis massa sed elementum tempus. Feugiat in ante metus dictum at - tempor. Malesuada nunc vel risus commodo viverra maecenas accumsan. - Integer vitae justo.`, - }, -}; - -const ProfilePage = () => { - const [editMode, setEditMode] = useState(false); - const [profileFound, setProfileFound] = useState(false); - const [profile, setProfile] = useState(null); - - const state = useGlobalContext(); - const { loggedIn } = state; - - const changeEditMode = () => { - setEditMode(!editMode); - }; - - const { id } = state; - - const fetchProfile = async () => { - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/getProfessorProfile/${id}` - ); - - if (response) { - let data = await response.json(); - setProfile(data); - setProfileFound(true); - } else { - setProfileFound(false); - } - }; - - useEffect(() => { - if (id) { - const tempProfile = PROFILES[id]; - - if (tempProfile) { - setProfile(tempProfile); - setProfileFound(true); - } - } - }, []); - - var editButton = ( - - ); - - const profilePage = ( -
-
- - -
- -
- ); - - return ( -
-
- {!loggedIn ? ( - "Please log in to view your profile" - ) : profileFound ? ( - <> - {loggedIn && editButton} - {loggedIn && editMode && } - {loggedIn && !editMode && profilePage} - - ) : ( - "Profile not found" - )} -
-






-
- ); -}; - -export default ProfilePage; diff --git a/src/shared/pages/Profile.tsx b/src/shared/pages/Profile.tsx new file mode 100644 index 00000000..5859efaf --- /dev/null +++ b/src/shared/pages/Profile.tsx @@ -0,0 +1,66 @@ +import React, { useEffect } from "react"; +import { useState } from "react"; +import ProfileComponents from "../components/Profile/ProfileComponents.tsx"; +import { useAuth } from "../../context/AuthContext.tsx"; +// import EditProfile from "./EditProfile"; + +export default function ProfilePage() { + const { auth } = useAuth(); + + if (!auth.isAuthenticated) { + window.location.href = "/login"; + } + + // const [editMode, setEditMode] = useState(false); + interface Profile { + id: string; + name: string; + image: string; + department: string; + description: string; + website?: string; + } + + const [profile, setProfile] = useState(null); + + // const changeEditMode = () => { + // setEditMode(!editMode); + // }; + + useEffect(() => { + const fetchProfile = async () => { + const response = await fetch( + `${process.env.REACT_APP_BACKEND_SERVER}/profile`, { + headers: { + Authorization: `Bearer ${auth.token}`, + }, + } + ); + + if (response.ok) { + const data = await response.json(); + // if (data.lab_manager) { + // window.location.href = "/staff/" + data.id; + // } + setProfile(data); + } else { + setProfile(false); + } + }; + fetchProfile(); + }, [auth.token]); + + // const editButton = ( + // + // ); + + return ( +
+ {profile === null && "Loading..."} + {profile && typeof profile === "object" && } + {profile === false && "Profile not found"} +
+ ); +}; diff --git a/src/staff/components/DepartmentItems.tsx b/src/staff/components/DepartmentItems.tsx index e0862184..02b07f54 100644 --- a/src/staff/components/DepartmentItems.tsx +++ b/src/staff/components/DepartmentItems.tsx @@ -8,7 +8,7 @@ const DepartmentItems = ({ items }) => { {items.map((item) => { return ( { DepartmentItems.propTypes = { items: PropTypes.arrayOf( PropTypes.shape({ - id: PropTypes.string.isRequired, + school_id: PropTypes.string.isRequired, department_id: PropTypes.string.isRequired, title: PropTypes.string.isRequired, image: PropTypes.string.isRequired, diff --git a/src/staff/pages/Departments.tsx b/src/staff/pages/Departments.tsx index ac6df925..8235e638 100644 --- a/src/staff/pages/Departments.tsx +++ b/src/staff/pages/Departments.tsx @@ -11,7 +11,7 @@ export default function Departments() { } const [departments, setDepartments] = useState< - { id: string; department_id: string; title: string; image: string }[] | string | null + { school_id: string; department_id: string; title: string; image: string }[] | string | null >(null); useEffect(() => { diff --git a/src/staff/pages/Staff.tsx b/src/staff/pages/Staff.tsx index 7c4cabd2..5f7abf12 100644 --- a/src/staff/pages/Staff.tsx +++ b/src/staff/pages/Staff.tsx @@ -1,77 +1,54 @@ import React, { useState, useEffect } from "react"; -import ProfileAvatar from "../../shared/components/UIElements/ProfileAvatar.tsx"; -import ProfileDescription from "../components/ProfileDescription.tsx"; -import ProfileOpportunities from "../components/ProfileOpportunities.tsx"; +import ProfileComponents from "../../shared/components/Profile/ProfileComponents.tsx"; import { useParams } from "react-router"; -import SEO from "../../shared/components/SEO.tsx"; import { useAuth } from "../../context/AuthContext.tsx"; export default function StaffPage() { const { auth } = useAuth(); - if (!auth.isAuthenticated) { - window.location.href = "/login"; - } - const { staffId } = useParams(); - const [profile, setProfile] = useState<{ name?: string; image?: string; department?: string; description?: string; website?: string } | "not found" | null>(null); - - const checkProfile = (data) => { - return data.name && data.image && data.department && data.description; - }; + const [profile, setProfile] = useState(null); - const fetchProfile = async () => { - const response = await fetch( - `${process.env.REACT_APP_BACKEND_SERVER}/staff/${staffId}`, { - headers: { - Authorization: `Bearer ${auth.token}`, - }, - } - ); + useEffect(() => { + const fetchProfile = async () => { + const response = await fetch( + `${process.env.REACT_APP_BACKEND_SERVER}/staff/${staffId}`, { + headers: { + Authorization: `Bearer ${auth.token}`, + }, + } + ); - if (!response.ok) { - setProfile("not found"); - } else { - const data = await response.json(); - if (checkProfile(data)) { - setProfile(data); + if (!response.ok) { + setProfile(false); } else { - setProfile("not found"); - console.log(data); + const data = await response.json(); + if (checkProfile(data)) { + setProfile(data); + } else { + setProfile(false); + console.log(data); + } } - } - }; - - useEffect(() => { - fetchProfile(); - }, []); + }; - const profileComponents = ( -
-
- {typeof profile === "object" && profile !== null && ( - - )} - {typeof profile === "object" && profile !== null && ( - - )} -
- {staffId && } -
- ); + if (!auth.isAuthenticated) { + window.location.href = "/login"; + } else if (!staffId) { + setProfile(false); + } else { + fetchProfile(); + } + const checkProfile = (data: { name: string; image: string; department: string; description: string }) => { + return data.name && data.image && data.department && data.description; + }; + }, [auth.token, staffId, auth.isAuthenticated]); return ( <> - - {!profile && "Loading..."} - {typeof profile === "object" && profileComponents} - {profile === "not found" && "Profile not found"} + {profile === null && "Loading..."} + {profile && typeof profile === "object" && staffId && } + {profile === false &&

Profile not found

} ); };