diff --git a/sports-matcher/client/.eslintrc.json b/sports-matcher/client/.eslintrc.json index 3cdde28..456e459 100644 --- a/sports-matcher/client/.eslintrc.json +++ b/sports-matcher/client/.eslintrc.json @@ -35,6 +35,7 @@ "semi": [ "error", "always" - ] + ], + "no-unused-vars": "warn" } } \ No newline at end of file diff --git a/sports-matcher/client/src/Layout.js b/sports-matcher/client/src/Layout.js index 7f2a32e..ccc66d2 100644 --- a/sports-matcher/client/src/Layout.js +++ b/sports-matcher/client/src/Layout.js @@ -1,7 +1,7 @@ import "./styles/Layout.css"; import "./styles/extra.css"; -import React from "react"; -import { NavLink, Route, Routes } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { NavLink, Route, Routes, useNavigate } from "react-router-dom"; import Welcome from "./pages/Welcome"; import Navbar from "react-bootstrap/Navbar"; import { Container, Nav, NavbarBrand } from "react-bootstrap"; @@ -10,11 +10,56 @@ import NavbarCollapse from "react-bootstrap/esm/NavbarCollapse"; import Dashboard from "./pages/Dashboard"; import Admin from "./pages/NewAdmin"; import Login from "./pages/Login"; +import { apiClient } from "./utils/httpClients"; +import { globalContext } from "./context.js"; -export default class Layout extends React.Component { - render() { - return ( -
+ +export default function layout() { + const navigate = useNavigate(); + const [state, setState] = useState({ + user: null, + }); + + useEffect(async () => { + await updateAuthStatus(); + }); + + async function updateAuthStatus() { + const getUserResponse = await apiClient.get("/user"); + if (getUserResponse !== 200) { + setState({ user: null }); + } else { + setState({ user: getUserResponse.data }); + } + } + + let indentityDisplay = ( + + ); + + if (state.user) { + indentityDisplay = ( + + ); + } + + return ( +
+
@@ -26,6 +71,7 @@ export default class Layout extends React.Component { Home + {indentityDisplay} @@ -41,7 +87,7 @@ export default class Layout extends React.Component { -
- ); - } + +
+ ); } \ No newline at end of file diff --git a/sports-matcher/client/src/components/MatchInfoCard.js b/sports-matcher/client/src/components/MatchInfoCard.js index f99832f..f7deb91 100644 --- a/sports-matcher/client/src/components/MatchInfoCard.js +++ b/sports-matcher/client/src/components/MatchInfoCard.js @@ -9,7 +9,6 @@ export default class MatchInfoCard extends React.Component { getParticipants() { let participants = []; - console.log(this.props); this.props.match.participants.forEach(user => { participants.push(user.firstName); }); diff --git a/sports-matcher/client/src/components/MatchInfoCardDisplay.js b/sports-matcher/client/src/components/MatchInfoCardDisplay.js index 3dd3f7b..cb19099 100644 --- a/sports-matcher/client/src/components/MatchInfoCardDisplay.js +++ b/sports-matcher/client/src/components/MatchInfoCardDisplay.js @@ -1,7 +1,7 @@ import React from "react"; import propTypes from "prop-types"; import MatchInfoCard from "./MatchInfoCard"; - +import "../styles/MatchInfoCardDisplay.css"; export default class MatchInfoCardDisplay extends React.Component { constructor(props) { super(props); diff --git a/sports-matcher/client/src/components/SportInfoCard.js b/sports-matcher/client/src/components/SportInfoCard.js new file mode 100644 index 0000000..14477e9 --- /dev/null +++ b/sports-matcher/client/src/components/SportInfoCard.js @@ -0,0 +1,26 @@ +import React from "react"; +import { Card } from "react-bootstrap"; +import propTypes from "prop-types"; +export default class SportInfoCard extends React.Component { + constructor(props) { + super(props); + } + + render() { + return ( + + + {this.props.sport.name} + {this.props.sport.minPlayers.toString()} + + {this.props.sport.description} + + + + ); + } +} + +SportInfoCard.propTypes = { + sport: propTypes.object, +}; \ No newline at end of file diff --git a/sports-matcher/client/src/components/SportInfoCardDisplay.js b/sports-matcher/client/src/components/SportInfoCardDisplay.js new file mode 100644 index 0000000..c50e71d --- /dev/null +++ b/sports-matcher/client/src/components/SportInfoCardDisplay.js @@ -0,0 +1,24 @@ +import React from "react"; +import propTypes from "prop-types"; +import SportInfoCard from "./SportInfoCard"; +import "../styles/MatchInfoCardDisplay.css"; +export default class SportInfoCardDisplay extends React.Component { + constructor(props) { + super(props); + } + render() { + let sports = null; + if (this.props.recommendedsports.length > 0) { + sports = this.props.recommendedsports.map((sport) => ); + } + return ( +
+ {sports} +
+ ); + } +} + +SportInfoCardDisplay.propTypes = { + recommendedsports: propTypes.array, +}; \ No newline at end of file diff --git a/sports-matcher/client/src/context.js b/sports-matcher/client/src/context.js new file mode 100644 index 0000000..8f6e61c --- /dev/null +++ b/sports-matcher/client/src/context.js @@ -0,0 +1,3 @@ +import React from "react"; + +export const globalContext = React.createContext({}); diff --git a/sports-matcher/client/src/pages/Dashboard.js b/sports-matcher/client/src/pages/Dashboard.js index 0d6b3a6..a8370a0 100644 --- a/sports-matcher/client/src/pages/Dashboard.js +++ b/sports-matcher/client/src/pages/Dashboard.js @@ -3,7 +3,10 @@ import { Button, InputGroup, FormControl } from "react-bootstrap"; import "../styles/Dashboard.css"; import { apiClient } from "../utils/httpClients.js"; import MatchInfoCardDisplay from "../components/MatchInfoCardDisplay"; -import { needUser } from "../utils/routing.js"; +import SportInfoCardDisplay from "../components/SportInfoCardDisplay"; +import { globalContext } from "../context"; +import { needUser } from "../utils/routing"; + export default class Dashboard extends React.Component { constructor(props) { super(props); @@ -13,11 +16,14 @@ export default class Dashboard extends React.Component { displayedEquipment: [], user: null }; - this.getFirstName(); } + + static contextType = globalContext; + async componentDidMount() { - this.setState({ user: await needUser() }); // needUser says this page needs a user, and therefore, if there isn't a user, get them to login first. It returns the authenticated user. - this.setState({ displayedMatches: await this.latestMatches() }); + await needUser(this.context.navigate); + await this.latestMatches(); + await this.availableSports(); } async latestMatches() { let recentMatchesRes = await apiClient.get("/match/recent/15"); @@ -26,26 +32,13 @@ export default class Dashboard extends React.Component { } } - async availableMatches() { - let availableMatchesRes = await apiClient.get("/sports"); - if (availableMatchesRes.status === 200) { - this.setState({ displayedSports: availableMatchesRes.data.recent }); + async availableSports() { + let availableSportsRes = await apiClient.get("/sport"); + if (availableSportsRes.status === 200) { + this.setState({ displayedSports: availableSportsRes.data }); } } - async availableEquipment() { - let availableEquipmentRes = await apiClient.get("/rentals"); - if (availableEquipmentRes.status === 200) { - this.setState({ displayedEquipment: availableEquipmentRes.data.recent }); - } - } - - async getFirstName() { - // let result = await apiClient.post("/user/login", {"email": "johndoe@gmail.com", "password": "csc309h1"}).then(apiClient.get("/user")); - let user = await apiClient.get("/user"); - let tags = document.getElementsByTagName("h1"); - tags[0].innerHTML = user.firstName; - } render() { return ( @@ -66,12 +59,10 @@ export default class Dashboard extends React.Component {

Available Sports

- -
-
-

Available Equipment

- +
+ +
); } diff --git a/sports-matcher/client/src/pages/Login.js b/sports-matcher/client/src/pages/Login.js index a74018d..526852e 100644 --- a/sports-matcher/client/src/pages/Login.js +++ b/sports-matcher/client/src/pages/Login.js @@ -1,40 +1,93 @@ import React from "react"; -import { Button, Card, Form } from "react-bootstrap"; +import { Alert, Button, Card, Container, Form } from "react-bootstrap"; +import { globalContext } from "../context"; import { apiClient } from "../utils/httpClients"; import { guard } from "../utils/routing"; export default class Login extends React.Component { constructor(props) { super(props); + this.state = { + email: "", + password: "", + errorDisplayed: false, + }; + + this.attemptLogin = this.attemptLogin.bind(this); } + static contextType = globalContext; + async componentDidMount() { - const getUserResponse = await apiClient.get("/user"); - guard(() => getUserResponse.status === 401, "/dashboard"); // If it's not 401, then we redirect to dashboard. + try { + const getUserResponse = await apiClient.get("/user"); + guard(this.context.navigate, () => getUserResponse.status === 401, "/dashboard"); // If it's not 401, then we redirect to dashboard. + } catch (error) { + if (error.message !== "Request failed with status code 401") { + throw error; + } + } + } + + async attemptLogin(e) { + e.preventDefault(); + const loginResponse = await apiClient.post("/user/login", { + email: this.state.email, + password: this.state.password, + }, { + validateStatus: function (status) { + return status === 200 || status === 401 || status === 400; + } + }); + if (loginResponse.status === 200) { + this.context.navigate("/dashboard", { replace: true }); + } else if (loginResponse.status === 401) { + this.setState({ errorDisplayed: true }); + } } render() { + let errorMsg = ( +
+ ); + if (this.state.errorDisplayed) { + errorMsg = ( + < Alert variant="danger" onClose={() => this.setState({ errorDisplayed: false })} dismissible > + Incorrect credentials +

Double check your provided e-mail and password!

+ + ); + } + return ( -
- - - Login - Welcome back! -
- - E-mail - - - - Password - - - -
-
-
+
+ {errorMsg} + + + + Login + Welcome back! +
+ + E-mail + { + this.setState({ email: e.target.value }); + }} /> + + + Password + { + this.setState({ password: e.target.value }); + }} /> + + +
+
+
+
); } diff --git a/sports-matcher/client/src/pages/Logout.js b/sports-matcher/client/src/pages/Logout.js new file mode 100644 index 0000000..517d95c --- /dev/null +++ b/sports-matcher/client/src/pages/Logout.js @@ -0,0 +1,36 @@ +import React from "react"; +import { useNavigate } from "react-router-dom"; +import { apiClient } from "../utils/httpClients"; + +export default class Logout extends React.Component { + constructor(props) { + super(props); + } + + async componentDidMount() { + const logoutResponse = await apiClient.get("/user/logout"); + let navigation = useNavigate(); + if (logoutResponse.status === 401) { + navigation("/dashboard", { replace: true }); + } else { + this.redirectTimer = setTimeout(() => { + navigation("/", { replace: true }); + }, 2000); + } + } + + async componentWillUnmount() { + clearTimeout(this.redirectTimer); + } + + render() { + return ( +
+
+

You are now logged out. See you later!

+

We will redirect you shortly...

+
+
+ ); + } +} \ No newline at end of file diff --git a/sports-matcher/client/src/pages/SIgnup.js b/sports-matcher/client/src/pages/SIgnup.js new file mode 100644 index 0000000..c603579 --- /dev/null +++ b/sports-matcher/client/src/pages/SIgnup.js @@ -0,0 +1,88 @@ +import React from "react"; +import { Button, Card, Form } from "react-bootstrap"; +import { apiClient } from "../utils/httpClients"; +import { guard } from "../utils/routing"; + +export default class Signup extends React.Component { + constructor(props) { + super(props); + this.state = { + user: null, + alertShow: false, + alertKey: null, + alertMsg: null + } + this.state.user = { + email: null, + firstName: null, + lastName: null, + phone: null, + password: null + } + } + + async registerUser() { + const res = await apiClient.post("/user", this.state); + if (res.status === 200) { + this.warnUser("You are successfully signed up!", "success") + } else if (res === 409) { + this.warnUser("This user already exists. Try logging in instead.", "danger") + } else if (res === 400) { + this.warnUser("Missing required fields.", "danger") + } else { + this.warnUser("Internal server error. Please try again later.", "danger") + } + } + + setUserState(event) { + newUser = this.state.user; + newUser[event.target.controlId] = event.target.value + this.setState({user: newUser}) + } + + warnUser(msg, key) { + this.setState({alertMsg: msg}) + this.setState({show: true}) + } + + render() { + return ( +
+ + {this.state.alertMsg} + + + + Login + Welcome to Sports Matcher! +
+ + First name + + + + Last name + + + + E-mail + + + + Phone number + + + + Password + + + +
+
+
+
+ ); + } +} diff --git a/sports-matcher/client/src/styles/MatchInfoCardDisplay.css b/sports-matcher/client/src/styles/MatchInfoCardDisplay.css new file mode 100644 index 0000000..d6f7190 --- /dev/null +++ b/sports-matcher/client/src/styles/MatchInfoCardDisplay.css @@ -0,0 +1,4 @@ +.horizontal-scroller{ + display: flex; + overflow-x: auto; +} \ No newline at end of file diff --git a/sports-matcher/client/src/utils/httpClients.js b/sports-matcher/client/src/utils/httpClients.js index e512953..36a9ea2 100644 --- a/sports-matcher/client/src/utils/httpClients.js +++ b/sports-matcher/client/src/utils/httpClients.js @@ -1,6 +1,10 @@ import axios from "axios"; export const apiClient = axios.create({ - baseURL: process.env.REACT_APP_API_HOST, + baseURL: (process.env.REACT_APP_API_HOST || "") + "/api/", timeout: 5000, + withCredentials: process.env.NODE_ENV === "development", + validateStatus: function (status) { + return status === 401 || status == 200; + } }); \ No newline at end of file diff --git a/sports-matcher/client/src/utils/routing.js b/sports-matcher/client/src/utils/routing.js index 94c328e..46caad9 100644 --- a/sports-matcher/client/src/utils/routing.js +++ b/sports-matcher/client/src/utils/routing.js @@ -1,7 +1,6 @@ -import { useNavigate } from "react-router-dom"; import { apiClient } from "./httpClients"; -export function guard(evaluator, redirect, navigateOptions, onRedirect) { +export function guard(navigator, evaluator, redirect, navigateOptions, onRedirect) { if (!evaluator) throw new Error("evaluator required."); if (!redirect) throw new Error("redirect required."); if (!navigateOptions) { @@ -9,16 +8,15 @@ export function guard(evaluator, redirect, navigateOptions, onRedirect) { replace: true }; } - let navigate = useNavigate(); let redirecting = !evaluator(); if (redirecting) { if (onRedirect) onRedirect(); - navigate(redirect, navigateOptions); + navigator(redirect, navigateOptions); } } -export async function needUser() { +export async function needUser(navigator) { let userDataResponse = await apiClient.get("/user"); - guard(() => userDataResponse.status === 200, "/login"); + guard(navigator, () => userDataResponse.status === 200, "/login"); return userDataResponse.data; } \ No newline at end of file diff --git a/sports-matcher/server/.eslintrc.json b/sports-matcher/server/.eslintrc.json index 0806fb5..e296e45 100644 --- a/sports-matcher/server/.eslintrc.json +++ b/sports-matcher/server/.eslintrc.json @@ -14,7 +14,7 @@ 4 ], "linebreak-style": [ - "error", + "warn", "unix" ], "quotes": [ @@ -24,6 +24,7 @@ "semi": [ "error", "always" - ] + ], + "no-unused-vars": "warn" } } \ No newline at end of file diff --git a/sports-matcher/server/server.js b/sports-matcher/server/server.js index dcb9da6..6e74fd5 100644 --- a/sports-matcher/server/server.js +++ b/sports-matcher/server/server.js @@ -27,9 +27,9 @@ try { if (process.env.NODE_ENV === "development") { + console.log("We are running in development mode."); mongoose.set("bufferCommands", false); // We want to know if there are connection issues immediately for development. Disables globally. - - server.use(cors()); + server.use(cors({ credentials: true, origin: "http://localhost:3000" })); } // Docs: https://www.npmjs.com/package/body-parser @@ -38,10 +38,10 @@ server.use(bodyParser.urlencoded({ extended: true })); server.use(userSession); -server.use("/user", UserController); -server.use("/match", MatchController); -server.use("/sport", SportController); -server.use("/rental", rentalController); +server.use("/api/user", UserController); +server.use("/api/match", MatchController); +server.use("/api/sport", SportController); +server.use("/api/rental", rentalController); server.listen(port, () => { console.log(`Server listening on port ${port}.`);