diff --git a/client/src/Icons/ContactIcon.jsx b/client/src/Icons/ContactIcon.jsx new file mode 100644 index 000000000..91925391b --- /dev/null +++ b/client/src/Icons/ContactIcon.jsx @@ -0,0 +1,32 @@ +import React from 'react'; + +const styles = { + Icon: { + fill: '#8a1c1c', + top: '464px', + left: '407px', + width: '114px', + height: '133px', + }, +}; + +export const IconComponent = ({ className = '' }) => ( + + + + +); + +const defaultProps = { + IconComponent, +}; + +const ContactIcon = (props) => { + return ( + props.IconComponent + ? + : + ); +}; + +export default ContactIcon; \ No newline at end of file diff --git a/client/src/img/coordinator.jpg b/client/src/img/coordinator.jpg new file mode 100644 index 000000000..590722dfd Binary files /dev/null and b/client/src/img/coordinator.jpg differ diff --git a/client/src/img/ipms-banner.jpg b/client/src/img/ipms-banner.jpg new file mode 100644 index 000000000..8737e6f2f Binary files /dev/null and b/client/src/img/ipms-banner.jpg differ diff --git a/client/src/img/mansoor.jpg b/client/src/img/mansoor.jpg new file mode 100644 index 000000000..c48f1c2a7 Binary files /dev/null and b/client/src/img/mansoor.jpg differ diff --git a/client/src/img/student.jpg b/client/src/img/student.jpg new file mode 100644 index 000000000..af635da68 Binary files /dev/null and b/client/src/img/student.jpg differ diff --git a/client/src/img/supervisor.jpg b/client/src/img/supervisor.jpg new file mode 100644 index 000000000..0b7065ed6 Binary files /dev/null and b/client/src/img/supervisor.jpg differ diff --git a/client/src/pages/A1InternshipRequestForm.js b/client/src/pages/A1InternshipRequestForm.js index ee307f08e..d3d855fd1 100644 --- a/client/src/pages/A1InternshipRequestForm.js +++ b/client/src/pages/A1InternshipRequestForm.js @@ -1,6 +1,6 @@ import React, { useState, useEffect } from "react"; import "../styles/A1InternshipRequestForm.css"; - +import { useNavigate } from "react-router-dom"; const outcomeLabels = [ "Problem Solving", @@ -11,6 +11,7 @@ const outcomeLabels = [ "Application", ]; + const outcomeDescriptions = [ "Understand and solve complex computing problems", "Create, build, and assess computing solutions", @@ -25,7 +26,7 @@ const signatureFonts = [ { name: "Great Vibes", class: "font-great-vibes" }, { name: "Pacifico", class: "font-pacifico" }, { name: "Satisfy", class: "font-satisfy" }, - { name: "Caveat", class: "font-caveat" } + { name: "Caveat", class: "font-caveat" }, ]; @@ -61,9 +62,11 @@ const SignatureInput = ({ id, value, onChange, disabled, placeholder }) => { /> {showFonts && nameInput && (
-
Select a signature style:
+
+ Select a signature style: +
{signatureFonts.map((font) => ( -
selectFont(font.class)} @@ -76,11 +79,7 @@ const SignatureInput = ({ id, value, onChange, disabled, placeholder }) => { {nameInput && (
{nameInput} - +
)}
@@ -88,6 +87,7 @@ const SignatureInput = ({ id, value, onChange, disabled, placeholder }) => { }; const A1InternshipRequestForm = ({ userRole = "student" }) => { + const navigate = useNavigate(); const initialState = { interneeName: "", // soonerId: "", @@ -108,6 +108,31 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { }; const [formData, setFormData] = useState(initialState); + + useEffect(() => { + const ouEmail = localStorage.getItem("ouEmail"); + + fetch( + `${ + process.env.REACT_APP_API_URL + }/api/student/me?ouEmail=${encodeURIComponent(ouEmail)}` + ) + .then((res) => { + console.log("Fetch status:", res.status); + return res.json(); + }) + .then((data) => { + console.log("Fetch returned:", data); + setFormData((f) => ({ + ...f, + interneeName: data.interneeName, + soonerId: data.soonerId, + interneeEmail: data.interneeEmail, + })); + }) + .catch((err) => console.error("Prefill error:", err)); + }, []); + const [successMsg, setSuccessMsg] = useState(""); const [errors, setErrors] = useState({}); const [dateError, setDateError] = useState(""); @@ -115,11 +140,18 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const isFieldEditable = (fieldType) => { switch (userRole) { case "student": - return !["advisorSignature", "coordinatorApproval", "supervisorComments", "coordinatorComments"].includes(fieldType); + return ![ + "advisorSignature", + "coordinatorApproval", + "supervisorComments", + "coordinatorComments", + ].includes(fieldType); case "supervisor": return ["advisor", "supervisorComments"].includes(fieldType); case "coordinator": - return ["coordinator", "coordinatorComments", "advisor"].includes(fieldType); + return ["coordinator", "coordinatorComments", "advisor"].includes( + fieldType + ); default: return true; } @@ -149,7 +181,7 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { } } if (errors[id]) { - setErrors(prev => { + setErrors((prev) => { const newErrors = { ...prev }; delete newErrors[id]; return newErrors; @@ -176,7 +208,9 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { useEffect(() => { const timeout = setTimeout(() => { - const descriptions = formData.tasks.map((task) => task.description.trim()).filter(Boolean); + const descriptions = formData.tasks + .map((task) => task.description.trim()) + .filter(Boolean); if (descriptions.length > 0) { fetch(`${process.env.REACT_APP_API_URL}/api/align-outcomes`, { method: "POST", @@ -186,8 +220,12 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { .then((res) => res.json()) .then((data) => { const updatedTasks = formData.tasks.map((task) => { - const match = data.results.find((r) => r.task === task.description); - return match ? { ...task, outcomes: match.matched_outcomes } : { ...task, outcomes: [] }; + const match = data.results.find( + (r) => r.task === task.description + ); + return match + ? { ...task, outcomes: match.matched_outcomes } + : { ...task, outcomes: [] }; }); setFormData((prev) => ({ ...prev, tasks: updatedTasks })); }) @@ -198,10 +236,14 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { }, [formData.tasks]); const renderOutcomeCell = (task, outcome, key) => { - const normalizedOutcome = outcome.charAt(0).toLowerCase() + outcome.replace(/\s+/g, "").slice(1); + const normalizedOutcome = + outcome.charAt(0).toLowerCase() + outcome.replace(/\s+/g, "").slice(1); const isMatched = task.outcomes.includes(normalizedOutcome); return ( - + { //if (!formData.soonerId) newErrors.soonerId = "Sooner ID is required"; //else if (!numberPattern.test(formData.soonerId)) newErrors.soonerId = "Sooner ID should be numeric"; if (!formData.interneeEmail) newErrors.interneeEmail = "Email is required"; - else if (!emailPattern.test(formData.interneeEmail)) newErrors.interneeEmail = "Invalid email format"; - if (!formData.workplaceName) newErrors.workplaceName = "Workplace name is required"; - else if (!namePattern.test(formData.workplaceName)) newErrors.workplaceName = "Workplace name should contain only letters and spaces"; - if (formData.website && !formData.website.includes('.')) newErrors.website = "Please enter a valid website address"; + else if (!emailPattern.test(formData.interneeEmail)) + newErrors.interneeEmail = "Invalid email format"; + if (!formData.workplaceName) + newErrors.workplaceName = "Workplace name is required"; + else if (!namePattern.test(formData.workplaceName)) + newErrors.workplaceName = + "Workplace name should contain only letters and spaces"; + if (formData.website && !formData.website.includes(".")) + newErrors.website = "Please enter a valid website address"; if (!formData.phone) newErrors.phone = "Phone is required"; - else if (!phonePattern.test(formData.phone)) newErrors.phone = "Phone must be 10 digits"; + else if (!phonePattern.test(formData.phone)) + newErrors.phone = "Phone must be 10 digits"; if (!formData.startDate) newErrors.startDate = "Start date is required"; if (!formData.endDate) newErrors.endDate = "End date is required"; if (!formData.advisorName) newErrors.advisorName = "Supervisor name is required"; @@ -254,11 +302,14 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const submitFormData = async () => { try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/form/submit`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(formData), - }); + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/form/submit`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + } + ); if (!response.ok) throw new Error("Failed to submit form"); const data = await response.json(); return data; @@ -270,11 +321,14 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const sendTaskDescriptions = async (descriptions) => { try { - const response = await fetch(`${process.env.REACT_APP_API_URL}/api/align-outcomes`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tasks: descriptions }), - }); + const response = await fetch( + `${process.env.REACT_APP_API_URL}/api/align-outcomes`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tasks: descriptions }), + } + ); if (!response.ok) throw new Error("Failed to send task descriptions"); const data = await response.json(); return data.results.map(({ task, matched_outcomes }) => ({ @@ -290,16 +344,27 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { const handleSubmit = async (e) => { e.preventDefault(); if (!validateForm()) return; - const taskDescriptions = formData.tasks.map((task) => task.description.trim()).filter(Boolean); + const taskDescriptions = formData.tasks + .map((task) => task.description.trim()) + .filter(Boolean); try { const aligned = await sendTaskDescriptions(taskDescriptions); if (aligned && aligned.length > 0) { setFormData((prev) => ({ ...prev, tasks: aligned })); const submissionResponse = await submitFormData(); - const recipient = submissionResponse.manual ? "coordinator for manual review!" : "supervisor!"; + const recipient = submissionResponse.manual + ? "coordinator for manual review!" + : "supervisor!"; setSuccessMsg(`Form submitted successfully and sent to ${recipient}`); setTimeout(() => setSuccessMsg(""), 15000); setFormData(initialState); + + const timeoutId = setTimeout(() => { + navigate('/student-dashboard'); + }, 1000); + + + return () => clearTimeout(timeoutId); } else { setErrors({ tasks: "Outcome alignment failed or returned no tasks." }); } @@ -310,22 +375,22 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { useEffect(() => { const fonts = [ - 'https://fonts.googleapis.com/css2?family=Dancing+Script:wght@500&display=swap', - 'https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap', - 'https://fonts.googleapis.com/css2?family=Pacifico&display=swap', - 'https://fonts.googleapis.com/css2?family=Satisfy&display=swap', - 'https://fonts.googleapis.com/css2?family=Caveat:wght@500&display=swap' + "https://fonts.googleapis.com/css2?family=Dancing+Script:wght@500&display=swap", + "https://fonts.googleapis.com/css2?family=Great+Vibes&display=swap", + "https://fonts.googleapis.com/css2?family=Pacifico&display=swap", + "https://fonts.googleapis.com/css2?family=Satisfy&display=swap", + "https://fonts.googleapis.com/css2?family=Caveat:wght@500&display=swap", ]; const links = []; - fonts.forEach(font => { - const link = document.createElement('link'); + fonts.forEach((font) => { + const link = document.createElement("link"); link.href = font; - link.rel = 'stylesheet'; + link.rel = "stylesheet"; document.head.appendChild(link); links.push(link); }); return () => { - links.forEach(link => document.head.removeChild(link)); + links.forEach((link) => document.head.removeChild(link)); }; }, []); @@ -354,29 +419,41 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { // disabled={!isFieldEditable("interneeName")} disabled /> - {errors.interneeName &&
{errors.interneeName}
} + {errors.interneeName && ( +
+ {errors.interneeName} +
+ )} Name*:
- - {errors.workplaceName &&
{errors.workplaceName}
} + {errors.workplaceName && ( +
+ {errors.workplaceName} +
+ )} Name*:
- - {errors.advisorName &&
{errors.advisorName}
} + {errors.advisorName && ( +
+ {errors.advisorName} +
+ )} @@ -427,37 +504,49 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { disabled /> - {errors.interneeEmail &&
{errors.interneeEmail}
} + {errors.interneeEmail && ( +
+ {errors.interneeEmail} +
+ )} Phone*:
- - {errors.phone &&
{errors.phone}
} + {errors.phone && ( +
+ {errors.phone} +
+ )} Email*:
- - {errors.advisorEmail &&
{errors.advisorEmail}
} + {errors.advisorEmail && ( +
+ {errors.advisorEmail} +
+ )} Credit Hours*:
- - {errors.creditHours &&
{errors.creditHours}
} + {errors.creditHours && ( +
+ {errors.creditHours} +
+ )} -
-
- -
- {errors.startDate &&
{errors.startDate}
} - - - -
-
- -
- {dateError &&
{dateError}
} - {errors.endDate &&
{errors.endDate}
} - + +
+
+ +
+ {errors.startDate && ( +
{errors.startDate}
+ )} + + + +
+
+ +
+ {dateError &&
{dateError}
} + {errors.endDate && ( +
{errors.endDate}
+ )} + -

Task Details & Program Outcomes*

+

+ Task Details & Program Outcomes + * +

Job Description Details: @@ -522,7 +623,8 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { Task {outcomeLabels.map((label, j) => ( - {label}
+ {label} +
({outcomeDescriptions[j]}) ))} @@ -537,16 +639,26 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { placeholder={`Task ${i + 1}`} value={task.description} onChange={(e) => handleTaskChange(i, e.target.value)} - style={{ width: "100%", padding: "4px", boxSizing: "border-box" }} + style={{ + width: "100%", + padding: "4px", + boxSizing: "border-box", + }} disabled={!isFieldEditable("task")} /> - {outcomeLabels.map((label, j) => renderOutcomeCell(task, label, `${i}-${j}`))} + {outcomeLabels.map((label, j) => + renderOutcomeCell(task, label, `${i}-${j}`) + )} ))} - {errors.tasks &&
{errors.tasks}
} + {errors.tasks && ( +
+ {errors.tasks} +
+ )}

Signatures:

@@ -556,7 +668,7 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { Internee Signature*:
- { placeholder="Enter your full name" />
- {errors.interneeSignature &&
{errors.interneeSignature}
} + {errors.interneeSignature && ( +
+ {errors.interneeSignature} +
+ )} @@ -581,4 +697,4 @@ const A1InternshipRequestForm = ({ userRole = "student" }) => { ); }; -export default A1InternshipRequestForm; \ No newline at end of file +export default A1InternshipRequestForm; diff --git a/client/src/pages/About.js b/client/src/pages/About.js new file mode 100644 index 000000000..346545515 --- /dev/null +++ b/client/src/pages/About.js @@ -0,0 +1,69 @@ +import React from "react"; +import "../styles/About.css"; +import bannerImg from "../img/ipms-banner.jpg"; +import studentImg from '../img/student.jpg'; +import supervisorImg from '../img/supervisor.jpg'; +import coordinatorImg from '../img/coordinator.jpg'; + +const About = () => { + return ( +
+

About IPMS

+ + IPMS banner + +

+ The Internship Program Management System (IPMS) is an all-in-one solution designed to streamline and support every stage of the Computer Science internship experience at the University of Oklahoma. +

+ +
+
+
+ ๐Ÿ” +

Role-based Login

+

Students, supervisors, and coordinators get access to tools tailored to their role.

+
+
+ ๐Ÿงพ +

Token-based Authentication

+

Secure token login system โ€” no passwords. Tokens last the full semester.

+
+
+ ๐Ÿ”„ +

Streamlined Communication

+

Automatic reminders and workflows connect all users seamlessly.

+
+
+
+ +
+

Who Uses IPMS?

+
+
+ student +

Students

+

Submit requests, track weekly progress, and complete your internship journey.

+
+
+ supervisor +

Supervisors

+

Review forms, approve progress, and evaluate student performance.

+
+
+ Coordinator +

Coordinators

+

Oversee submissions, grade final reports, and manage workflows.

+
+
+
+ +
+

Get in Touch

+

Interested in learning more or need assistance? Contact us!

+ Contact Page +
+
+ ); +}; + +export default About; \ No newline at end of file diff --git a/client/src/pages/Contact.js b/client/src/pages/Contact.js new file mode 100644 index 000000000..9de515013 --- /dev/null +++ b/client/src/pages/Contact.js @@ -0,0 +1,91 @@ +import React from "react"; +import "../styles/Contact.css"; +import MansoorImage from "../img/mansoor.jpg"; +import ContactIcon, { IconComponent } from "../Icons/ContactIcon"; + +const teamMembers = [ + { + name: "Gladis Menachery Sunny", + email: "Gladis.Menachery.Sunny-1@ou.edu", + role: "Token Generation and Login" + }, + { + name: "Ravichandra Reddy", + email: "Ravichandra.Reddy.Mulagondla-1@ou.edu", + role: "A.1 Form" + }, + { + name: "Naveena Suddapalli", + email: "Naveena.Suddapalli-1@ou.edu", + role: "A.2 Weekly Report" + }, + { + name: "Saketh Reddy Aredla", + email: "Saketh.Reddy.Aredla-1@ou.edu", + role: "A.3 Form" + }, + { + name: "Rahul Juluru", + email: "Rahul.Juluru-1@ou.edu", + role: "Supervisor approval workflow" + }, + { + name: "Narayana Phani Charan Nimmagadda", + email: "Narayana.Phani.Charan.Nimmagadda-1@ou.edu", + role: "Coordinator Approval workflow" + }, +]; + +const MansoorEmail = "Mansoor.A.Abdulhak-1@ou.edu"; +const AssistantEmail = "Oluwasijibomi.Ajisegiri@ou.edu"; + +const Contact = () => { + return ( +
+

Contact Us

+ + {/* Academic Contact */} +
+ Dr. Mansoor Abdulhak +
+

Dr. Mansoor Abdulhak

+

Assistant Professor, University of Oklahoma

+

๐Ÿ“ง {MansoorEmail}

+

๐Ÿ“ž (405) 325-5408

+

๐Ÿข Devon Energy Hall, 234

+
+
+ + {/* Teaching Assistant */} +
+ } /> +
+

SJ Ajisegiri

+

Teaching Assistant

+

๐Ÿ“ง {AssistantEmail}

+
+
+ + {/* Technical Support */} +
+

Technical Support โ€“ Team G

+
+ {teamMembers.map(({ name, email, role }) => ( +
+ } /> +
+
{name}
+

{role}

+

+ ๐Ÿ“ง {email} +

+
+
+ ))} +
+
+
+ ); +}; + +export default Contact; \ No newline at end of file diff --git a/client/src/pages/Home.js b/client/src/pages/Home.js index ffe18418f..5f27617b0 100644 --- a/client/src/pages/Home.js +++ b/client/src/pages/Home.js @@ -37,7 +37,6 @@ function Home() { const { email: ouEmail, password, role } = formData; - if (!ouEmail || !password || !role) { return Swal.fire({ icon: "warning", @@ -55,31 +54,34 @@ function Home() { "Content-Type": "application/json", }, body: JSON.stringify({ ouEmail, password, role }), - }, + } ); const data = await response.json(); if (response.ok) { const user = data.user; - if(role === "student"){ - // Store only required fields - const limitedUserInfo = { - fullName: user.fullName, - id: user._id, - email:user.ouEmail - }; - - localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo)); - navigate("/student-dashboard"); - }else if(role === "supervisor"){ + if (role === "student") { + // Store only required fields + const limitedUserInfo = { + fullName: user.fullName, + id: user._id, + email: user.ouEmail, + academicAdvisor: user.academicAdvisor, + semester: user.semester, + }; + + localStorage.setItem("ipmsUser", JSON.stringify(limitedUserInfo)); + localStorage.setItem("ouEmail", user.ouEmail); + navigate("/student-dashboard"); + } else if (role === "supervisor") { Swal.fire({ icon: "success", title: "Login Successful ๐ŸŒŸ", text: `Welcome back, ${role}!`, }); navigate("/supervisor-dashboard"); - }else{ + } else { Swal.fire({ icon: "success", title: "Login Successful ๐ŸŒŸ", @@ -88,23 +90,21 @@ function Home() { navigate("/coordinator-dashboard"); } - - // Swal.fire({ // icon: "success", // title: "Login Successful", // text: `Welcome back, `, // }); - - } else { Swal.fire({ icon: "error", title: "Login Failed", - html: data.message + " " + - (data.renewalLink - ? `Please click here to request a new token.` - : "Something went wrong."), + html: + data.message + + " " + + (data.renewalLink + ? `Please click here to request a new token.` + : "Something went wrong."), }); } } catch (error) { @@ -151,7 +151,6 @@ function Home() { role: r, }) } - >

@@ -219,16 +218,7 @@ function Home() { Remember me - - Forgot password? - +

- {/* ------ FORM A2 Card ------ */} + {/* FORM A2 Card */}
-

Weekly Report (Form A2)

- - {approvalStatus === "not_submitted" && ( -

- Please fill your Form A1 first -

- )} - - {approvalStatus === "draft" && ( -

- Finish your Form A1 first -

- )} - - {(approvalStatus === "submitted" || - approvalStatus === "pending manual review") && ( -

- Wait for your Form A1 to be approved -

- )} +

Weekly Report (FORM A2)

+

+ {approvalStatus === "approved" + ? "You may now submit weekly reports" + : "Finish Form A1 approval first"} +

- +
+ + + {/* Submissions Section */} +
+

My Submissions

+ {submissions.length === 0 ? ( +

No submissions yet.

+ ) : ( + + + + + + + + + + + {submissions.map((s) => ( + + + + + + + ))} + +
FormSupervisor StatusCoordinator StatusActions
{s.form_type}{s.supervisor_status}{s.coordinator_status} + {s.supervisor_status === "approved" && + s.coordinator_status === "pending" ? ( + <> + + + + ) : ( + "โ€”" + )} +
+ )} +
+
@@ -138,4 +310,4 @@ const StudentDashboard = () => { ); }; -export default StudentDashboard; +export default StudentDashboard; \ No newline at end of file diff --git a/client/src/router.js b/client/src/router.js index 2dba3de27..a41675cf9 100644 --- a/client/src/router.js +++ b/client/src/router.js @@ -19,6 +19,8 @@ import CoordinatorRequestDetailView from "./pages/CoordinatorRequestDetailView"; import TokenRenewal from "./pages/TokenRenewal"; import StudentDashboard from "./pages/StudentDashboard"; import ProtectedRouteStudent from "./pages/ProtectedRouteStudent"; +import About from "./pages/About"; +import Contact from "./pages/Contact"; import WeeklyFourWeekReportForm from "./pages/WeeklyFourWeekReportForm"; import SubmittedReports from "./pages/SubmittedReports"; import CumulativeReviewForm from "./pages/CumulativeReviewForm"; @@ -84,6 +86,14 @@ const router = createBrowserRouter([ path: "renew-token/:token", element: , }, + { + path: "about", + element: , + }, + { + path: "contact", + element: , + }, { path: "four-week-report", element: , diff --git a/client/src/styles/About.css b/client/src/styles/About.css new file mode 100644 index 000000000..4a6ba6b8c --- /dev/null +++ b/client/src/styles/About.css @@ -0,0 +1,112 @@ + .about-wrapper { + max-width: 1100px; + margin: auto; + padding: 2rem; + font-family: 'Segoe UI', sans-serif; + color: #333; + } + + .about-heading { + text-align: center; + font-size: 2.8rem; + margin-bottom: 1rem; + color: #2a4d69; + } + + .about-banner { + width: 100%; + height: auto; + border-radius: 8px; + margin-bottom: 1.5rem; + } + + .about-summary { + font-size: 1.1rem; + text-align: center; + margin-bottom: 2rem; + line-height: 1.6; + } + + .section { + margin-bottom: 3rem; + } + + .feature-cards { + display: flex; + gap: 1.5rem; + justify-content: space-around; + flex-wrap: wrap; + } + + .feature-card { + background: #fff; + border: 1px solid #ddd; + border-radius: 12px; + padding: 1.5rem; + width: 300px; + text-align: center; + box-shadow: 0 4px 8px rgba(0,0,0,0.05); + transition: 0.3s ease; + } + + .feature-card:hover { + box-shadow: 0 6px 15px rgba(0,0,0,0.1); + } + + .feature-card .icon { + font-size: 3rem; + margin-bottom: 0.5rem; + } + + .section-heading { + text-align: center; + font-size: 1.8rem; + margin-bottom: 1rem; + } + + .user-role-cards { + display: flex; + justify-content: space-around; + gap: 1.5rem; + flex-wrap: wrap; + } + + .user-card { + background: #fff; + padding: 1.2rem; + text-align: center; + border-radius: 12px; + border: 1px solid #ddd; + width: 250px; + box-shadow: 0 4px 8px rgba(0,0,0,0.05); + } + + .user-card img { + width: 200px; + margin-bottom: 0.8rem; + } + + .cta-footer { + background-color: #8b0000; + color: white; + text-align: center; + padding: 2rem; + border-radius: 8px; + } + + .cta-button { + display: inline-block; + margin-top: 1rem; + padding: 0.8rem 1.6rem; + background: white; + color: #8b0000; + text-decoration: none; + font-weight: bold; + border-radius: 6px; + transition: background 0.3s ease; + } + + .cta-button:hover { + background: #f0f0f0; + } + \ No newline at end of file diff --git a/client/src/styles/Contact.css b/client/src/styles/Contact.css new file mode 100644 index 000000000..e1f1dc745 --- /dev/null +++ b/client/src/styles/Contact.css @@ -0,0 +1,80 @@ +.contact-container { + max-width: 900px; + margin: 0 auto; + padding: 2rem; + font-family: 'Segoe UI', sans-serif; + background-color: #f9f9fb; +} + +.contact-heading { + text-align: center; + font-size: 2.5rem; + margin-bottom: 2rem; + color: #222; +} + +.contact-card, +.tech-support-card { + background: #fff; + border-radius: 12px; + padding: 1.5rem; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + margin-bottom: 1.5rem; +} + +.contact-card { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.contact-image { + width: 160px; + height: 160px; + border-radius: 50%; + object-fit: cover; +} + +.contact-info { + flex: 1; +} + +.contact-info h2 { + margin-bottom: 0.25rem; + color: #222; +} + +.contact-info a { + color: #9d2235; + text-decoration: none; +} + +.contact-info a:hover { + text-decoration: underline; +} + +.team-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(375px, 1fr)); + gap: 1.2rem; + margin-top: 1rem; +} + +.team-member { + display: flex; + align-items: center; + gap: 0.8rem; +} + +.team-name { + font-weight: 600; + color: #333; +} + +.team-icon { + width: 114px; + height: 133px; + fill: #8a1c1c; + flex-shrink: 0; + display: block; +} \ No newline at end of file diff --git a/server/models/TokenRequest.js b/server/models/TokenRequest.js index d7b52670e..2b76e0a77 100644 --- a/server/models/TokenRequest.js +++ b/server/models/TokenRequest.js @@ -21,8 +21,7 @@ const mongoose = require('mongoose'); * - deletedAt: Marks soft deletion if the student cancels. * - status: Optional string enum for tracking token state. * - activationLinkSentAt: Timestamp when the activation email was sent. - * - password: Encrypted password for login authentication. - + * * Additional Features: * - Automatically sets `expiresAt` to 6 months from `requestedAt`. * - Uses `timestamps` to auto-generate `createdAt` and `updatedAt`. @@ -79,10 +78,11 @@ const userTokenRequestSchema = new mongoose.Schema( }, token: { type: String, - required: [true, 'Token is required'], - unique: true, + required: function () { + return this.role === "student"; + }, + // Note: unique index will be handled separately below }, - isActivated: { type: Boolean, default: false, @@ -105,7 +105,7 @@ const userTokenRequestSchema = new mongoose.Schema( }, status: { type: String, - enum: ['pending', 'activated', 'expired', 'deleted','deactivated'], + enum: ['pending', 'activated', 'expired', 'deleted', 'deactivated'], default: 'pending', }, }, @@ -114,6 +114,20 @@ const userTokenRequestSchema = new mongoose.Schema( } ); +userTokenRequestSchema.index( + { token: 1 }, + { + unique: true, + partialFilterExpression: { isStudent: true, token: { $exists: true, $ne: null } } + } +); +userTokenRequestSchema.index( + { soonerId: 1 }, + { + unique: true, + partialFilterExpression: { isStudent: true, soonerId: { $exists: true, $ne: null } } + } +); // Automatically set expiresAt to 5 days after requestedAt userTokenRequestSchema.pre('save', function (next) { if (!this.expiresAt) { @@ -124,12 +138,16 @@ userTokenRequestSchema.pre('save', function (next) { next(); }); +// Auto-expire unactivated requests after 5 days userTokenRequestSchema.index( { requestedAt: 1 }, { - expireAfterSeconds: 432000, + expireAfterSeconds: 432000, // 5 days partialFilterExpression: { isActivated: false }, } ); -module.exports = mongoose.model('UserTokenRequest', userTokenRequestSchema); \ No newline at end of file +// โœ… NEW: Make token unique only if token exists (Partial Unique Index) + + +module.exports = mongoose.model('UserTokenRequest', userTokenRequestSchema); diff --git a/server/models/User.js b/server/models/User.js index 59aecfdd8..ee8181c4c 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -18,6 +18,9 @@ const userSchema = new mongoose.Schema({ type: String, required: true, }, + soonerId: { + type: String, + }, createdAt: { type: Date, default: Date.now, @@ -25,4 +28,3 @@ const userSchema = new mongoose.Schema({ }); module.exports = mongoose.model("User", userSchema); - diff --git a/server/routes/studentRoutes.js b/server/routes/studentRoutes.js index 26716b7c2..70311336e 100644 --- a/server/routes/studentRoutes.js +++ b/server/routes/studentRoutes.js @@ -4,27 +4,31 @@ const InternshipRequest = require("../models/InternshipRequest"); const User = require("../models/User"); const TokenRequest = require("../models/TokenRequest"); - // GET internship request by student's ouEmail router.post("/", async (req, res) => { const { ouEmail } = req.body; console.log("Received email:", ouEmail); try { - const studentUser = await TokenRequest.findOne({ ouEmail }); - - if (!studentUser) { - return res.status(404).json({ message: "Student not found in TokenRequest" }); - } - - const internshipData = await InternshipRequest.findOne({ student: studentUser._id }); + const internshipData = await InternshipRequest.findOne({ + "student.email": ouEmail, + }); + if (!internshipData) { // No record found, return a specific flag - return res.status(200).json({ message: "No internship request found", approvalStatus: "not_submitted" }); + return res.status(200).json({ + message: "No internship request found", + approvalStatus: "not_submitted", + }); } + const { supervisor_status, coordinator_status } = internshipData; + - const approvalStatus = internshipData.status; + const approvalStatus = + (supervisor_status == "pending" || supervisor_status == "approved" ) ? supervisor_status : coordinator_status + + console.log(supervisor_status, coordinator_status) return res.status(200).json({ message: "Success", approvalStatus }); } catch (error) { @@ -33,6 +37,54 @@ router.post("/", async (req, res) => { } }); - +// Fetch interneeName, soonerId, interneeEmail by OU email passed as query string +router.get("/me", async (req, res) => { + const { ouEmail } = req.query; + if (!ouEmail) { + return res + .status(400) + .json({ message: "Missing query parameter: ouEmail" }); + } + + try { + // look in the TokenRequest collection where your student doc lives + const student = await TokenRequest.findOne({ ouEmail }).select( + "fullName soonerId ouEmail" + ); + + if (!student) { + return res.status(404).json({ message: "Student not found" }); + } + + return res.json({ + interneeName: student.fullName, + soonerId: student.soonerId, + interneeEmail: student.ouEmail, + }); + } catch (err) { + console.error("Error in GET /api/student/me:", err); + return res.status(500).json({ message: "Server error" }); + } +}); + +// DELETE /api/student/account/:id +router.delete("/account/:id", async (req, res) => { + try { + const studentId = req.params.id; + + await TokenRequest.findByIdAndDelete(studentId); + + await InternshipRequest.deleteMany({ student: studentId }); + + return res + .status(200) + .json({ message: "Account and related data deleted" }); + } catch (err) { + console.error("Error deleting account:", err); + return res + .status(500) + .json({ message: "Server error while deleting account" }); + } +}); -module.exports = router; \ No newline at end of file +module.exports = router; diff --git a/server/routes/token.js b/server/routes/token.js index 5b663911b..6c8387995 100644 --- a/server/routes/token.js +++ b/server/routes/token.js @@ -5,7 +5,7 @@ const crypto = require("crypto"); const bcrypt = require("bcrypt"); const TokenRequest = require("../models/TokenRequest"); const emailService = require("../services/emailService"); -const User = require("../models/User") +const User = require("../models/User"); const JWT_SECRET = process.env.JWT_SECRET; const FRONTEND_URL = process.env.FRONTEND_URL; @@ -15,10 +15,13 @@ const hashToken = (token) => { return crypto.createHash("sha256").update(token).digest("hex"); }; +// ---------------------------------- TOKEN REQUEST ---------------------------------- +// ---------------------------------- TOKEN REQUEST ---------------------------------- router.post("/request", async (req, res) => { try { const { fullName, ouEmail, soonerId, password, semester, academicAdvisor, role } = req.body; - if (!fullName || !ouEmail || !password || !semester) { + + if (!fullName || !ouEmail || !password || !semester || !role) { return res.status(400).json({ error: "All fields are required." }); } @@ -27,71 +30,84 @@ router.post("/request", async (req, res) => { return res.status(401).json({ error: "Token request already exists for this email." }); } - if(role==="student"){ + if (role.toLowerCase() === "student") { + if (!soonerId) { + return res.status(400).json({ error: "Sooner ID is required for students." }); + } + const existingSoonerId = await TokenRequest.findOne({ soonerId }); - if(existingSoonerId){ + if (existingSoonerId) { return res.status(402).json({ error: "Token request already exists for this Sooner ID." }); } } - const plainToken = jwt.sign({ ouEmail }, JWT_SECRET, { expiresIn: "180d" }); - const hashedToken = hashToken(plainToken); - const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); + let plainToken = null; + let hashedToken = null; + const isStudent = role.toLowerCase() === "student"; - const requestedAt = new Date(); - const expiresAt = new Date(requestedAt.getTime() + 5 * 24 * 60 * 60 * 1000); + if (role.toLowerCase() === "student") { + plainToken = jwt.sign({ ouEmail }, JWT_SECRET, { expiresIn: "180d" }); + hashedToken = hashToken(plainToken); + } + + const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS); const request = new TokenRequest({ fullName, ouEmail, - soonerId: role === "student" ? soonerId : "", + soonerId: isStudent ? soonerId : undefined, password: hashedPassword, semester, role, - academicAdvisor: role === "student" ? academicAdvisor : "", - isStudent: role === "student", - token: hashedToken, - requestedAt, - expiresAt, - activationLinkSentAt: new Date(), + academicAdvisor: isStudent ? academicAdvisor : undefined, + isStudent, + ...(isStudent && { + token: hashedToken, + activationLinkSentAt: new Date(), + }) }); - - await request.save(); - - - const activationLink = `${FRONTEND_URL}/activate/${plainToken}`; - const emailBody = ` -

Hi ${fullName},

-

Thank you for requesting access to the Internship Program Management System (IPMS).

-

Your activation link:

-

${activationLink}

-

Note: This token will expire in 5 days if not activated.

-

Regards,
IPMS Team

- `; - - await emailService.sendEmail({ - to: ouEmail, - subject: "Your IPMS Token Activation Link", - html: emailBody, - }); - res.status(201).json({ - message: "Token requested and email sent.", - token: plainToken, - expiresAt, - }); + if (role.toLowerCase() === "student") { + const activationLink = `${FRONTEND_URL}/activate/${plainToken}`; + const emailBody = ` +

Hi ${fullName},

+

Thank you for requesting access to the Internship Program Management System (IPMS).

+

Your activation link:

+

${activationLink}

+

Note: This token will expire in 5 days if not activated.

+

Regards,
IPMS Team

+ `; + + await emailService.sendEmail({ + to: ouEmail, + subject: "Your IPMS Token Activation Link", + html: emailBody, + }); + + res.status(201).json({ + message: "Token requested and email sent.", + token: plainToken, + }); + } else { + console.log(`Email not sent - user is not a student: ${ouEmail}`); + res.status(201).json({ message: "Token is not required for supervisors or coordinators." }); + } } catch (err) { console.error("Token Request Error:", err); res.status(500).json({ error: err.message }); } }); +// ---------------------------------- ACTIVATE TOKEN ---------------------------------- router.post("/activate", async (req, res) => { try { + console.log(" Activation request received at backend"); + const { token } = req.body; if (!token) return res.status(400).json({ error: "Token is missing." }); + const hashedToken = hashToken(token); const user = await TokenRequest.findOne({ token: hashedToken }); @@ -109,13 +125,16 @@ router.post("/activate", async (req, res) => { user.expiresAt = sixMonthsLater; await user.save(); + console.log("Token activated successfully"); res.json({ message: "Token activated successfully." }); } catch (err) { + console.error("Activation error:", err); res.status(500).json({ error: err.message }); } }); +// ---------------------------------- LOGIN BY TOKEN (Optional) ---------------------------------- router.post("/login", async (req, res) => { try { const { token } = req.body; @@ -133,6 +152,7 @@ router.post("/login", async (req, res) => { } }); +// ---------------------------------- USER LOGIN (Email/Password) ---------------------------------- router.post("/user-login", async (req, res) => { const { ouEmail, password, role } = req.body; @@ -144,7 +164,7 @@ router.post("/user-login", async (req, res) => { const user = await TokenRequest.findOne({ ouEmail }); if (!user) { - return res.status(401).json({ message: "Email or password is incorrect" }); + return res.status(401).json({ message: "Email does not Exist. Try Signing up." }); } const isMatch = await bcrypt.compare(password, user.password); @@ -173,13 +193,13 @@ router.post("/user-login", async (req, res) => { const tokenExpiry = new Date(user.expiresAt); if (tokenExpiry < now || user.status === "deactivated") { - if(!user.status === "deactivated"){ + if (user.status !== "deactivated") { user.status = "deactivated"; await user.save(); } return res.status(403).json({ - message : "Your account is deactivated due to token expiry.", - renewalLink: `${FRONTEND_URL}/renew-token/${user.token}` + message: "Your account is deactivated due to token expiry.", + renewalLink: `${FRONTEND_URL}/renew-token/${user.token}`, }); } } @@ -191,6 +211,28 @@ router.post("/user-login", async (req, res) => { } }); +router.delete("/delete", async (req, res) => { + try { + const { ouEmail } = req.body; + + if (!ouEmail) { + return res.status(400).json({ error: "Email is not found for deletion." }); + } + + const user = await TokenRequest.findOneAndDelete({ouEmail}); + + if (!user) { + return res.status(404).json({ error: "User not found." }); + } + + res.status(200).json({ message: "User deleted successfully." }); + + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ---------------------------------- DEACTIVATE TOKEN (SOFT DELETE) ---------------------------------- router.delete("/deactivate", async (req, res) => { try { const { token, ouEmail } = req.body; @@ -201,9 +243,6 @@ router.delete("/deactivate", async (req, res) => { let filter = {}; if (token) { - if (typeof token !== "string") { - return res.status(400).json({ error: "Token must be a string." }); - } const hashedToken = hashToken(token); filter = { token: hashedToken }; } else { @@ -230,6 +269,8 @@ router.delete("/deactivate", async (req, res) => { } }); +// ---------------------------------- TOKEN RENEWAL ---------------------------------- +// ---------------------------------- TOKEN RENEWAL ---------------------------------- router.post("/renew", async (req, res) => { try { const { token } = req.body; @@ -238,6 +279,7 @@ router.post("/renew", async (req, res) => { return res.status(400).json({ message: "Token is required." }); } + // const hashedToken = hashToken(token); const user = await TokenRequest.findOne({ token: token }); if (!user) { @@ -272,4 +314,4 @@ router.post("/renew", async (req, res) => { } }); -module.exports = router; +module.exports = router; \ No newline at end of file diff --git a/server/services/emailService.js b/server/services/emailService.js index e2b1118e8..b784325e0 100644 --- a/server/services/emailService.js +++ b/server/services/emailService.js @@ -18,8 +18,8 @@ class EmailService { }); this.defaultSender = - process.env.EMAIL_DEFAULT_SENDER || - "Internship Program Management System "; + process.env.EMAIL_DEFAULT_SENDER || + "Internship Program Management System "; } /** @@ -33,6 +33,7 @@ class EmailService { * @param {Array} [options.attachments] - Array of attachment objects * @param {Array} [options.cc] - Carbon copy recipients * @param {Array} [options.bcc] - Blind carbon copy recipients + * @param {string} [options.role] - User role (optional, to conditionally skip email) * @returns {Promise} - Result of the email sending operation */ async sendEmail(options) { @@ -44,6 +45,7 @@ class EmailService { }; } + const mailOptions = { from: options.from || this.defaultSender, to: options.to, @@ -52,7 +54,7 @@ class EmailService { text: options.text || options.html.replace(/<[^>]*>/g, ""), attachments: options.attachments || [], }; - + // Add optional fields if provided if (options.cc) mailOptions.cc = options.cc; if (options.bcc) mailOptions.bcc = options.bcc; @@ -66,6 +68,7 @@ class EmailService { } } } + // Create and export a singleton instance const emailService = new EmailService(); module.exports = emailService;