feat(InterviewBooking): added error handing, button css and defined select on date

This commit is contained in:
darkicewolf50 2025-04-12 12:03:14 -06:00
parent 6aade88454
commit aa8e5c2577
3 changed files with 412 additions and 320 deletions

View File

@ -1,84 +1,122 @@
:root { :root {
--interviewspacing: clamp(5px, 2.5svw, 200px); --interviewspacing: clamp(5px, 2.5svw, 200px);
--interviewwidth: 260px; --interviewwidth: 260px;
} }
#InterviewBooking header { #InterviewBooking header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
background-color: lightslategray; background-color: lightslategray;
justify-content: space-around; justify-content: space-around;
margin: 0px; margin: 0px;
} }
#InterviewForm { #InterviewForm {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
} }
#InterviewForm div { #InterviewForm div {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-left: var(--interviewspacing); padding-left: var(--interviewspacing);
padding-right: var(--interviewspacing); padding-right: var(--interviewspacing);
text-align: start; text-align: start;
width: var(--interviewwidth); width: var(--interviewwidth);
} }
#InterviewForm div label { #InterviewForm div label {
font-weight: 700; font-weight: 700;
} }
#InterviewForm div p { #InterviewForm div p {
font-size: x-small; font-size: x-small;
margin: 0px; margin: 0px;
} }
#InterviewForm div input { #InterviewForm div input {
margin-top: 1svh; margin-top: 1svh;
height: 30px; height: 30px;
} }
#MainForm { #MainForm {
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
#MainForm form { #MainForm form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
width: 80%; width: 80%;
} }
#TimeSlotSelector { #TimeSlotSelector {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
justify-content: center; justify-content: center;
} }
.TimeSlot { .TimeSlot {
padding-left: var(--interviewspacing); padding-left: var(--interviewspacing);
padding-right: var(--interviewspacing); padding-right: var(--interviewspacing);
width: var(--interviewwidth); width: var(--interviewwidth);
} }
.TimeSlot h4 { .TimeSlot h4 {
margin: 0px; margin: 0px;
margin-top: 30px; margin-top: 30px;
} }
.TimeSlot p { .TimeSlot p {
margin: 0px; margin: 0px;
margin-bottom: 20px; margin-bottom: 20px;
} }
#InterviewSubmit { #InterviewSubmit {
display: flex; display: flex;
justify-content: center; justify-content: center;
width: inherit; width: inherit;
} }
#InterviewSubmit button { #InterviewSubmit button {
background-color: lightgreen; background-color: lightgreen;
width: 20%; width: 20%;
height: 5svh; height: 5svh;
margin-bottom: 5svh; margin-bottom: 5svh;
font-size: large; font-size: large;
border: none; border: none;
} }
#InterviewText { #InterviewText {
padding-left: var(--interviewspacing); padding-left: var(--interviewspacing);
padding-right: var(--interviewspacing); padding-right: var(--interviewspacing);
width: 80%; width: 80%;
}
:root {
/* used for editing time buttons */
--TimeSlotSideWidth: 48%;
--TimeSlotSidePaddingTopBottom: 1svh 0px;
--TimeSlotSideMarginTopBottom: 0.25svh 1%;
}
.TimeSlotSide0 {
cursor: pointer;
border: none;
width: var(--TimeSlotSideWidth);
padding: var(--TimeSlotSidePaddingTopBottom);
margin: var(--TimeSlotSideMarginTopBottom);
}
.TimeSlotSide1 {
cursor: pointer;
border: none;
width: var(--TimeSlotSideWidth);
padding: var(--TimeSlotSidePaddingTopBottom);
margin: var(--TimeSlotSideMarginTopBottom);
}
#interviewLoading {
display: flex;
align-items: center;
justify-content: center;
text-align: center;
height: 334.3px;
}
#CurrentSelected {
background-color: lightseagreen;
} }

View File

@ -1,5 +1,5 @@
import { useState, useRef } from "react"; import { useState, useRef } from "react";
import TimeDateSelector from "./TimeDateSelector"; // Import the TimeSlotSelector component import TimeDateSelector from "./TimeDateSelector";
/** /**
* @param {null} null - Takes in nothing * @param {null} null - Takes in nothing
@ -10,149 +10,181 @@ import TimeDateSelector from "./TimeDateSelector"; // Import the TimeSlotSelecto
*/ */
const InterviewForm = () => { const InterviewForm = () => {
const [isButtonDisabled, setIsButtonDisabled] = useState(false); const [isButtonDisabled, setIsButtonDisabled] = useState(false);
const dialogRef = useRef(null); const dialogRef = useRef(null);
const [selectedTimeSlot, setSelectedTimeSlot] = useState(null); const [selectedTimeSlot, setSelectedTimeSlot] = useState(null);
const [getTimeDates, setGetTimeDates] = useState(""); const [getTimeDates, setGetTimeDates] = useState("");
/** /**
* @param {String HTML} event - Takes in form info * @param {String HTML} event - Takes in form info
* @returns {null} null - Returns in nothing * @returns {null} null - Returns in nothing
* @description * @description submits the form with the appropriate information
* @author Ahmad <ahmadmuhammadofficial@gmail.com> * @author Ahmad <ahmadmuhammadofficial@gmail.com>
* @todo CSS * @todo imporper email and other erros from backend
*/ */
const formsubmit = async (event) => { const formsubmit = async (event) => {
event.preventDefault(); const errorLine = document.getElementById("InterviewError");
if (!selectedTimeSlot) { event.preventDefault();
alert("Please select a time slot!");
return;
}
// disable button to stop multiple requests if (selectedTimeSlot) {
setIsButtonDisabled(true); console.log(selectedTimeSlot);
if (selectedTimeSlot.date !== null && selectedTimeSlot.startTime !== "") {
errorLine.innerHTML = " ";
// disable button to stop multiple requests
setIsButtonDisabled(true);
// await new Promise((res) => setTimeout(res, 1000)); // await new Promise((res) => setTimeout(res, 1000));
const formData = new FormData(event.target); const formData = new FormData(event.target);
const formObject = Object.fromEntries(formData.entries()); const formObject = Object.fromEntries(formData.entries());
formObject.date = selectedTimeSlot["date"]; // Add the selected time slot to form data formObject.date = selectedTimeSlot["date"]; // Add the selected time slot to form data
formObject.startTime = selectedTimeSlot["startTime"]; formObject.startTime = selectedTimeSlot["startTime"];
console.log("Form Data:", formObject); console.log("Form Data:", formObject);
const res = await fetch(
"https://bajabackend.bajacloud.duckdns.org/SelectInterview",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formObject),
}
);
// const res = await fetch(
// "http://127.0.0.1:8000/SelectInterview",
// {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(formObject),
// }
// );
let data = await res.json(); const res = await fetch(
"https://bajabackend.bajacloud.duckdns.org/SelectInterview",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formObject),
}
);
// const res = await fetch(
// "http://127.0.0.1:8000/SelectInterview",
// {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify(formObject),
// }
// );
if (data["body"]["Success"] === true) { let data = await res.json();
dialogRef.current.showModal();
} else {
setGetTimeDates(getTimeDates + "i");
}
setIsButtonDisabled(false); if (data["body"]["Success"] === true) {
}; dialogRef.current.showModal();
} else {
setGetTimeDates(getTimeDates + "i");
}
x;
} else {
formSubmitTimeErorrs(selectedTimeSlot, errorLine);
}
} else {
formSubmitTimeErorrs(selectedTimeSlot, errorLine);
}
setIsButtonDisabled(false);
};
return ( /**
<> * @param {Object} missingError - Takes in object to find what is missing from it
<form onSubmit={formsubmit}> * @param {HTMLSelectElement} - display line on page
<div id="InterviewForm"> * @returns {null} null - Returns in nothing
<div> * @description A separated function to handle all timeslot errors possible states and displays the appropriate message on the error line
<label for="name">Name:</label> * @author Brock <darkicewolf50@gmail.com>
<input */
type="text" const formSubmitTimeErorrs = (missingError, errorLine) => {
id="fname" if (!missingError) {
name="intervieweeName" errorLine.innerHTML = "Please Select a Date and a Time";
placeholder="Jaeinceins" }
required // impossible state
/> /*
<p>(what to call you)</p> else if (missingError.date === null) {
</div> errorLine.innerHTML = "Please Select a Date";
<div> */
<label for="email">UCalgary Email:</label> else if (missingError.startTime) {
<input errorLine.innerHTML = "Please Select a Time";
type="text" }
id="email" };
name="intervieweeEmail" return (
placeholder="jaeinceins.bhaja@ucalgary.ca" <>
required <form onSubmit={formsubmit}>
/> <div id="InterviewForm">
<p>(for interview confirmation email)</p> <div>
</div> <label for="name">Name:</label>
</div> <input
type="text"
id="fname"
name="intervieweeName"
placeholder="Jaeinceins"
required
/>
<p>(what to call you)</p>
</div>
<div>
<label for="email">UCalgary Email:</label>
<input
type="text"
id="email"
name="intervieweeEmail"
placeholder="jaeinceins.bhaja@ucalgary.ca"
required
/>
<p>(for interview confirmation email)</p>
</div>
</div>
{/* Time Slot Selector */} {/* Time Slot Selector */}
<TimeDateSelector <TimeDateSelector
onTimeSlotSelect={(timeSlot) => setSelectedTimeSlot(timeSlot)} onTimeSlotSelect={(timeSlot) => setSelectedTimeSlot(timeSlot)}
timeDateSelectorGet={getTimeDates} timeDateSelectorGet={getTimeDates}
/> />
<div id="InterviewText"> <div id="InterviewText">
<h4> <h4>
What to do if I cannot make it to any of the avaliable time slots or What to do if I cannot make it to any of the avaliable time slots or
need to rescedule? need to rescedule?
</h4> </h4>
<p> <p>
While we highly encourage sceduling an interview in one of the above While we highly encourage sceduling an interview in one of the above
time slots, we recongize that not everyone can make it work with time slots, we recongize that not everyone can make it work with
their personal and university schedules. their personal and university schedules.
</p> </p>
<p> <p>
Please email us at{" "} Please email us at{" "}
<a href="mailto:uofcbaja@gmail.com">uofcbaja@gmail.com</a> to work <a href="mailto:uofcbaja@gmail.com">uofcbaja@gmail.com</a> to work
out an alternate interview time or for rescheduling. out an alternate interview time or for rescheduling.
</p> </p>
</div> </div>
<p id="InterviewError"> </p>
<div id="InterviewSubmit">
<button type="submit" disabled={isButtonDisabled}>
Submit
</button>
</div>
</form>
{/* Success Dialog */} <p id="InterviewError"> </p>
<dialog ref={dialogRef}>
{" "} <div id="InterviewSubmit">
{/* Add the `ref` attribute */} <button
<h2>Booking Successful!</h2> type="submit"
<p> disabled={isButtonDisabled}>
Thank you for booking your interview slot. Well contact you soon. Submit
</p> </button>
<h4> </div>
What to do if I cannot make it to any of the avaliable time slots or </form>
need to rescedule?
</h4> {/* Success Dialog */}
<p> <dialog ref={dialogRef}>
While we highly encourage sceduling an interview in one of the above {" "}
time slots, we recongize that not everyone can make it work with their {/* Add the `ref` attribute */}
personal and university schedules. <h2>Booking Successful!</h2>
</p> <p>
<p> Thank you for booking your interview slot. Well contact you soon.
Please email us at{" "} </p>
<a href="mailto:uofcbaja@gmail.com">uofcbaja@gmail.com</a> to work out <h4>
an alternate interview time or for rescheduling. What to do if I cannot make it to any of the avaliable time slots or
</p> need to rescedule?
</dialog> </h4>
</> <p>
); While we highly encourage sceduling an interview in one of the above
time slots, we recongize that not everyone can make it work with their
personal and university schedules.
</p>
<p>
Please email us at{" "}
<a href="mailto:uofcbaja@gmail.com">uofcbaja@gmail.com</a> to work out
an alternate interview time or for rescheduling.
</p>
</dialog>
</>
);
}; };
export default InterviewForm; export default InterviewForm;

View File

@ -2,145 +2,167 @@ import React, { useState, useEffect } from "react";
import DatePicker from "react-datepicker"; import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
/**
* @param {Function} onTimeSlotSelect - Used to pass back up the selected values from child component
* @param {Function} timeDateSelectorGet - Used to display dates avaialable
* @returns {JSX.element} JSX - HTML and JS functionality
* @description Used for picking an interview date
* @author Ahmad <ahmadmuhammadofficial@gmail.com>
*/
export default function TimeDateSelector({ export default function TimeDateSelector({
onTimeSlotSelect, onTimeSlotSelect,
timeDateSelectorGet, timeDateSelectorGet,
}) { }) {
const [allDatesAvailable, setAllDatesAvailable] = useState({}); const [allDatesAvailable, setAllDatesAvailable] = useState({});
const [selectedDate, setSelectedDate] = useState(null); const [selectedDate, setSelectedDate] = useState(null);
const [timeSlotsAvialable, setTimeSlotsAvialable] = useState([]); const [timeSlotsAvialable, setTimeSlotsAvialable] = useState([]);
const [selectedTime, setSelectedTime] = useState(""); const [selectedTime, setSelectedTime] = useState("");
const [selectedTimeButton, setSelectedTimeButton] = useState(null);
useEffect(() => { useEffect(() => {
getInterviewDates(); getInterviewDates();
}, [timeDateSelectorGet]); }, [timeDateSelectorGet]);
/** /**
* @param {null} null - Takes in nothing * @param {null} null - Takes in nothing
* @returns {null} null - Returns in nothing * @returns {null} null - Returns in nothing
* @description Gets interview timeslots and dates from backend * @description Gets interview timeslots and dates from backend
* @author Ahmad <ahmadmuhammadofficial@gmail.com>, Brock <darkicewolf50@gmail.com> * @author Ahmad <ahmadmuhammadofficial@gmail.com>, Brock <darkicewolf50@gmail.com>
* @todo refactor to not call backend so much, see useEffect above * @todo refactor to not call backend so much, see useEffect above
*/ */
const getInterviewDates = async () => { const getInterviewDates = async () => {
const res = await fetch( const res = await fetch(
"https://bajabackend.bajacloud.duckdns.org/getAppointments", "https://bajabackend.bajacloud.duckdns.org/getAppointments",
{ method: "GET" } { method: "GET" }
); );
// const res = await fetch( // const res = await fetch(
// "http://127.0.0.1:8000/getAppointments", // "http://127.0.0.1:8000/getAppointments",
// { method: "GET" } // { method: "GET" }
// ); // );
let json = await res.json(); let json = await res.json();
// console.log(json); // console.log(json);
// can input dates right away, no other requirements to show it // can input dates right away, no other requirements to show it
let dates = await json["body"]["interviewDates"]; let dates = await json["body"]["interviewDates"];
setAllDatesAvailable(await dates); setAllDatesAvailable(await dates);
}; };
// helper section // helper section
/** /**
* @param {Date} date - Takes in a date object from the date picker * @param {Date} date - Takes in a date object from the date picker
* @returns {null} null - Returns in nothing * @returns {null} null - Returns in nothing
* @description checks if date is available from the backend * @description checks if date is available from the backend
* @author Brock <darkicewolf50@gmail.com> * @author Brock <darkicewolf50@gmail.com>
*/ */
const isDateAvailable = (date) => { const isDateAvailable = (date) => {
return Object.keys(allDatesAvailable).includes( return Object.keys(allDatesAvailable).includes(
date.toISOString().split("T")[0] date.toISOString().split("T")[0]
); );
}; };
const handleDateChange = (date) => { const handleDateChange = (date) => {
setSelectedDate(date); // Capture the selected date in date object setSelectedDate(date); // Capture the selected date in date object
const selectedDateStr = date.toISOString().split("T")[0]; const selectedDateStr = date.toISOString().split("T")[0];
// get and set time slots for a given day // get and set time slots for a given day
setTimeSlotsAvialable(Object.keys(allDatesAvailable[selectedDateStr])); setTimeSlotsAvialable(Object.keys(allDatesAvailable[selectedDateStr]));
setSelectedTime(""); // clear because of date change setSelectedTime(""); // clear because of date change
}; // set prematurely for better error messages
onTimeSlotSelect({
date: selectedDateStr,
startTime: selectedTime,
});
};
const handleTimeSlotChange = (e) => { const handleTimeSlotChange = (e) => {
let startTime = e.target.innerHTML; e.currentTarget.id = "CurrentSelected";
setSelectedTime(startTime);
onTimeSlotSelect({ if (selectedTimeButton !== null) {
date: selectedDate.toLocaleDateString(), selectedTimeButton.id = "";
startTime: selectedTime, }
}); setSelectedTimeButton(e.currentTarget);
};
return ( let startTime = e.currentTarget.dataset.time;
<div id="TimeSlotSelector"> setSelectedTime(startTime);
{Object.keys(allDatesAvailable).length > 0 ? (
<> onTimeSlotSelect({
<div className="TimeSlot"> date: selectedDate.toLocaleDateString(),
<h4>Interview Date</h4> startTime: selectedTime,
<label htmlFor="date-picker"> });
<p>Select a Date:</p> };
</label>
<DatePicker return (
selected={selectedDate} <div id="TimeSlotSelector">
onChange={handleDateChange} {Object.keys(allDatesAvailable).length > 0 ? (
inline <>
filterDate={isDateAvailable} // Filter/grey out unavailable dates <div className="TimeSlot">
dateFormat="yyyy-MM-dd" <h4>Interview Date</h4>
required // Make date selection required <label htmlFor="date-picker">
/> <p>Select a Date:</p>
</div> </label>
<div className="TimeSlot"> <DatePicker
<h4>Interview Time</h4> selected={selectedDate}
{!selectedDate ? ( onChange={handleDateChange}
<> inline
<p>Available Time Slots:</p> filterDate={isDateAvailable} // Filter/grey out unavailable dates
<div dateFormat="yyyy-MM-dd"
style={{ required // Make date selection required
height: "241.633px", />
display: "flex", </div>
alignItems: "center", <div className="TimeSlot">
}} <h4>Interview Time</h4>
> {!selectedDate ? (
<p>Please select the a date to see time slots.</p> <>
</div> <p>Available Time Slots:</p>
</> <div
) : ( style={{
<> height: "241.633px",
<label htmlFor="time-picker"> display: "flex",
<p> alignItems: "center",
Available Time Slots for{" "} }}>
{selectedDate.toISOString().split("T")[0]}: <p>Please select the a date to see time slots.</p>
</p> </div>
</label> </>
{selectedDate === undefined ? ( ) : (
<> <>
<p>Please select a date.</p> <label htmlFor="time-picker">
</> <p>
) : timeSlotsAvialable !== "" ? ( Available Time Slots for{" "}
<> {selectedDate.toISOString().split("T")[0]}:
{Object.values(timeSlotsAvialable).map((time) => { </p>
return ( </label>
<button {selectedDate === undefined ? (
key={time} <>
type="button" <p>Please select a date.</p>
onClick={(self) => { </>
handleTimeSlotChange(self); ) : timeSlotsAvialable !== "" ? (
}} <>
> {Object.values(timeSlotsAvialable).map((time) => {
{time} // console.log(timeSlotsAvialable.indexOf(time));
</button> return (
); <button
})} className={
</> "TimeSlotSide" +
) : ( (timeSlotsAvialable.indexOf(time) % 2)
<> }
<p>No available time slots for the selected date.</p> key={time}
</> type="button"
)} data-time={time}
</> onClick={handleTimeSlotChange}>
)} {time.slice(0, 5)}
</div> </button>
{/* {selectedDate && selectedTime && ( );
})}
</>
) : (
<>
<p>No available time slots for the selected date.</p>
</>
)}
</>
)}
</div>
{/* {selectedDate && selectedTime && (
<div> <div>
<p> <p>
You have selected: You have selected:
@ -150,10 +172,10 @@ export default function TimeDateSelector({
Time: {selectedTime} Time: {selectedTime}
</p> </p>
</div>)} */} </div>)} */}
</> </>
) : ( ) : (
<p>Loading ...</p> <p id="interviewLoading">Loading ...</p>
)} )}
</div> </div>
); );
} }