diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index aa1f97680..c2e66efdc 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -22,6 +22,7 @@ jobs: pip install pytest==7.1.3 pip install flask-cors pip install BeautifulSoup4 + pip install Flask-Session2 - name: Analysing the code with pylint run: | pylint $(git ls-files '*.py') diff --git a/.gitignore b/.gitignore index 6ea2c11e2..12dd8c88a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ src/frontend/node_modules src/backend/.pytest_cache .pytest_cache src/backend/__pycache__ +src/backend/flask_session src/frontend/my-app/node_modules # testing diff --git a/src/backend/app.py b/src/backend/app.py index 25d1c7e66..f8990c8ad 100644 --- a/src/backend/app.py +++ b/src/backend/app.py @@ -1,18 +1,21 @@ """ Handles routing and HTTP Requests """ import json -import os import dataclasses from werkzeug.datastructures import MultiDict from flask import Flask, request, session from flask_cors import CORS +from flask_session import Session from pages.login import Login from pages.mainpage import MainPage from pages.userpage import UserPage from dataholders.mainpage_get import GetRequestType, GetRequestParams app = Flask(__name__) -app.config["SECRET_KEY"] = os.urandom(24) -CORS(app, resources={r"/*": {"origins": "*"}}) +SECRET_KEY = b"xe47Wxcdx86Wxac(mKlxa5xa2,xb3axc6xf1x86Fxc25x94xfc" +SESSION_TYPE = "filesystem" +app.config.from_object(__name__) +Session(app) +CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) @app.route("/login", methods=["GET", "POST"]) @@ -27,6 +30,7 @@ def login(): if user_login.login(username, password): # session object makes User accessible in the backend session["username"] = username + print(session.get("username")) return f"welcome {username}", 200 return "User not found, please try again", 401 return "", 400 @@ -45,6 +49,7 @@ def register(): result = user_login.register(username, email, password, phone) if not result.status: return result.message, 400 + session.pop("username", None) return result.message, 201 return "", 400 @@ -52,8 +57,9 @@ def register(): @app.route("/user", methods=["GET", "POST"]) def userpage(): """Handles userpage requests""" - if session.get("username", None) is None: - return "user does not exist", 404 + print(session.get("username")) + if session.get("username") is None: + return "user does not exist", 403 name = session.get("username") or "" page = UserPage(name) if request.method == "POST": @@ -78,7 +84,7 @@ def userpage(): return "success", 201 user = page.get_user(name) # request.method == "GET" data_dict = dataclasses.asdict(user) - return json.dumps(data_dict), 201 + return json.dumps(data_dict), 200 @app.route("/logout") @@ -91,8 +97,9 @@ def logout(): @app.route("/api/whoami") def whoami(): """Shows whether a user is logged in and returns session username""" - if session.get("username", None) is None: - return "user logged out", 404 + print(session.get("username")) + if session.get("username") is None: + return "user logged out", 403 username = session.get("username", "") return str(username), 201 diff --git a/src/backend/tests/auth.py b/src/backend/auth.py similarity index 100% rename from src/backend/tests/auth.py rename to src/backend/auth.py diff --git a/src/backend/database/database_prod.db b/src/backend/database/database_prod.db index caf6a1fa8..286110826 100644 Binary files a/src/backend/database/database_prod.db and b/src/backend/database/database_prod.db differ diff --git a/src/backend/database/database_test.db b/src/backend/database/database_test.db index 9b13870a4..6a2902beb 100644 Binary files a/src/backend/database/database_test.db and b/src/backend/database/database_test.db differ diff --git a/src/backend/pages/login.py b/src/backend/pages/login.py index c37fdd72a..8ff3101f7 100644 --- a/src/backend/pages/login.py +++ b/src/backend/pages/login.py @@ -1,7 +1,7 @@ """ Contains Login class """ from dataclasses import dataclass from decorators import use_database -from tests.auth import validate_email, validate_password, validate_phone +from auth import validate_email, validate_password, validate_phone @dataclass(frozen=True) @@ -33,7 +33,7 @@ def register( return RegisterResult("Invalid phone number, please try again", False) if not validate_password(password): - return RegisterResult("Password is too short, please try again", False) + return RegisterResult("Password length should be greater than 8", False) check = self.register.cursor.execute( "SELECT username FROM Users WHERE username = ?", (username,) diff --git a/src/backend/pages/mainpage.py b/src/backend/pages/mainpage.py index fe76caf68..e81ef52ca 100644 --- a/src/backend/pages/mainpage.py +++ b/src/backend/pages/mainpage.py @@ -220,7 +220,8 @@ def write_apartment_review( ) -> List[Review]: """Write a new review for apartment""" user_id = self.write_apartment_review.cursor.execute( - "SELECT user_id FROM Users WHERE username = ?", (username,) + "SELECT user_id FROM Users WHERE username = ? OR email = ?", + (username, username), ).fetchone()[0] today = date.today().strftime("%Y-%m-%d") self.write_apartment_review.cursor.execute( diff --git a/src/backend/pages/userpage.py b/src/backend/pages/userpage.py index 6c5187985..93634013c 100644 --- a/src/backend/pages/userpage.py +++ b/src/backend/pages/userpage.py @@ -1,6 +1,6 @@ """Contains the UserPage backend""" from typing import List -from tests.auth import validate_phone, validate_email, validate_password +from auth import validate_phone, validate_email, validate_password from dataholders.user import User from dataholders.apt import Apt from decorators import use_database @@ -9,24 +9,26 @@ class UserPage: """UserPage class""" - def __init__(self, username: str) -> None: + def __init__(self, name: str) -> None: """Constructor""" - self.username = username - self.user = self.get_user(username) + self.name = name + self.user = self.get_user(name) @use_database - def get_user(self, username: str) -> User: + def get_user(self, query_sql: str) -> User: """Return User object based on username""" - query_sql = username user_query = self.get_user.cursor.execute( - "SELECT u.user_id, u.password, u.email, u.phone \ + "SELECT u.user_id, u.username, u.password, u.email, u.phone \ FROM USERS u\ - WHERE u.username = ?", - (query_sql,), + WHERE u.username = ? OR u.email = ?", + ( + query_sql, + query_sql, + ), ).fetchone() if user_query is None: return User("", "", "", "", "") - user_id, password, email, phone = user_query + user_id, username, password, email, phone = user_query return User(user_id, username, password, email, phone) @use_database @@ -40,8 +42,8 @@ def update_password(self, password: str) -> bool: self.update_password.cursor.execute( "UPDATE Users \ SET password = ? \ - WHERE (username = ?)", - (password, self.username), + WHERE (username = ? OR email = ?)", + (password, self.name, self.name), ) self.user.password = password return True @@ -56,8 +58,8 @@ def update_email(self, email: str) -> bool: self.update_email.cursor.execute( "UPDATE Users \ SET email = ? \ - WHERE username = ?", - (email, self.username), + WHERE (username = ? OR email = ?)", + (email, self.name, self.name), ) self.user.email = email return True @@ -70,8 +72,9 @@ def update_phone(self, phone: str) -> bool: if self.user.phone == phone: return True self.update_phone.cursor.execute( - "UPDATE Users SET phone = ? WHERE (username = ?)", - (phone, self.username), + "UPDATE Users SET phone = ? \ + WHERE (username = ? OR email = ?)", + (phone, self.name, self.name), ) self.user.phone = phone return True diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt index ddd4af7d5..81e779734 100644 --- a/src/backend/requirements.txt +++ b/src/backend/requirements.txt @@ -3,4 +3,5 @@ pytest==7.1.3 pylint==2.15.2 coverage==6.4.4 black==22.8.0 -Flask_Cors==3.0.10 \ No newline at end of file +Flask_Cors==3.0.10 +Flask-Session2==1.3.1 \ No newline at end of file diff --git a/src/backend/tests/test_app.py b/src/backend/tests/test_app.py index 38e348c4e..7111ebf05 100644 --- a/src/backend/tests/test_app.py +++ b/src/backend/tests/test_app.py @@ -285,7 +285,7 @@ def test_userpage_not_logged_in(client): assert res.status_code == 404 with app.test_request_context("/user/"): res = userpage() - assert res[1] == 404 + assert res[1] == 403 @use_test @@ -301,7 +301,7 @@ def test_userpage_get_request(client): with app.test_request_context("/user/", method="GET"): session["username"] = "Mike" res = userpage() - assert res[1] == 201 + assert res[1] == 200 connection = sqlite3.connect("database/database_test.db") cursor = connection.cursor() cursor.execute("DELETE FROM Users WHERE username = ?", ("Mike",)) @@ -372,7 +372,7 @@ def test_whoami(): """Test whoami returns 404 and 201""" with app.test_request_context("/api/whoami"): res = whoami() - assert res[1] == 404 + assert res[1] == 403 with app.test_request_context("/api/whoami"): session["username"] = "Mike" res = whoami() diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 5ebd44a0a..818a95da2 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -30,6 +30,7 @@ "react-bootstrap": "^2.5.0", "react-dom": "^18.2.0", "react-infinite-scroll-component": "^6.1.0", + "react-material-ui-carousel": "3.4.2", "react-router-dom": "^6.4.2", "react-scripts": "^5.0.1", "styled-components": "^5.3.6", @@ -8362,6 +8363,48 @@ "url": "https://www.patreon.com/infusion" } }, + "node_modules/framer-motion": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", + "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "popmotion": "9.3.6", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": ">=16.8 || ^17.0.0", + "react-dom": ">=16.8 || ^17.0.0" + } + }, + "node_modules/framer-motion/node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/framer-motion/node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, + "node_modules/framesync": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", + "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -8706,6 +8749,11 @@ "he": "bin/he" } }, + "node_modules/hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -11915,6 +11963,17 @@ "node": ">=4" } }, + "node_modules/popmotion": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", + "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", + "dependencies": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + } + }, "node_modules/popper.js": { "version": "1.16.1-lts", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", @@ -13512,6 +13571,28 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "node_modules/react-material-ui-carousel": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/react-material-ui-carousel/-/react-material-ui-carousel-3.4.2.tgz", + "integrity": "sha512-jUbC5aBWqbbbUOOdUe3zTVf4kMiZFwKJqwhxzHgBfklaXQbSopis4iWAHvEOLcZtSIJk4JAGxKE0CmxDoxvUuw==", + "dependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@mui/icons-material": "^5.4.1", + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "framer-motion": "^4.1.17" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@mui/icons-material": "^5.0.0", + "@mui/material": "^5.0.0", + "@mui/system": "^5.0.0", + "react": "^17.0.1 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0" + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -14740,6 +14821,15 @@ "webpack": "^5.0.0" } }, + "node_modules/style-value-types": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", + "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", + "dependencies": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "node_modules/styled-components": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", @@ -22349,6 +22439,44 @@ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==" }, + "framer-motion": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", + "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", + "requires": { + "@emotion/is-prop-valid": "^0.8.2", + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "popmotion": "9.3.6", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + }, + "dependencies": { + "@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "requires": { + "@emotion/memoize": "0.7.4" + } + }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + } + } + }, + "framesync": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", + "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", + "requires": { + "tslib": "^2.1.0" + } + }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -22589,6 +22717,11 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "hey-listen": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -24959,6 +25092,17 @@ } } }, + "popmotion": { + "version": "9.3.6", + "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", + "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", + "requires": { + "framesync": "5.3.0", + "hey-listen": "^1.0.8", + "style-value-types": "4.1.4", + "tslib": "^2.1.0" + } + }, "popper.js": { "version": "1.16.1-lts", "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", @@ -25924,6 +26068,19 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-material-ui-carousel": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/react-material-ui-carousel/-/react-material-ui-carousel-3.4.2.tgz", + "integrity": "sha512-jUbC5aBWqbbbUOOdUe3zTVf4kMiZFwKJqwhxzHgBfklaXQbSopis4iWAHvEOLcZtSIJk4JAGxKE0CmxDoxvUuw==", + "requires": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@mui/icons-material": "^5.4.1", + "@mui/material": "^5.4.1", + "@mui/system": "^5.4.1", + "framer-motion": "^4.1.17" + } + }, "react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -26829,6 +26986,15 @@ "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz", "integrity": "sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ==" }, + "style-value-types": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", + "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", + "requires": { + "hey-listen": "^1.0.8", + "tslib": "^2.1.0" + } + }, "styled-components": { "version": "5.3.6", "resolved": "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 66ef98785..7fdd2d3e9 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -7,6 +7,7 @@ "@emotion/styled": "^11.10.4", "@material-ui/core": "^4.12.4", "@mui/icons-material": "^5.10.6", + "react-material-ui-carousel": "3.4.2", "@mui/material": "^5.10.7", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index 11b58054a..2980d6f04 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -5,7 +5,7 @@ import Header from './components/Header'; import Login from './pages/Login'; import MainPage from './pages/MainPage'; import Register from './pages/Register'; -// import UserPage from './pages/UserPage'; +import User from './pages/User'; function App() { return ( @@ -25,7 +25,7 @@ function App() { } /> - {/* } /> */} + } /> diff --git a/src/frontend/src/components/SearchBar.tsx b/src/frontend/src/components/SearchBar.tsx index 402f27adf..2e69e2558 100644 --- a/src/frontend/src/components/SearchBar.tsx +++ b/src/frontend/src/components/SearchBar.tsx @@ -1,4 +1,4 @@ -import { Autocomplete, Stack, TextField } from '@mui/material'; +import { Autocomplete, TextField } from '@mui/material'; import React, { useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import './SearchBarStyles.css'; @@ -29,22 +29,17 @@ export default function SearchBar() { }; return ( - <> -

Apartment Search

-
- - option.name)} - renderInput={(params) => ( - - )} - /> - -
-
- + + {/* Search bar with autocomplete from the server */} + option.name)} + renderInput={(params) => ( + + )} + /> + ); } diff --git a/src/frontend/src/components/SingleCard.tsx b/src/frontend/src/components/SingleCard.tsx index 7a5509017..d319da809 100644 --- a/src/frontend/src/components/SingleCard.tsx +++ b/src/frontend/src/components/SingleCard.tsx @@ -1,25 +1,20 @@ import React from 'react'; -import { Typography, Card, CardContent } from '@material-ui/core'; -import { CardActionArea } from '@mui/material'; -import styled from 'styled-components'; +import { + Typography, + Card, + CardContent, + Stack, + CardActionArea, +} from '@mui/material'; import { AptType } from './Types'; -const Container = styled.div``; - -const MyCard = styled(Card)` - margin-left: 20px; - margin-bottom: 20px; - height: 200px; - width: 470px; -`; - interface SingleCardProps { id: number; name: string; address: string; price_min: number; price_max: number; - votes: number; + rating: number; onSelect: (apt: AptType) => void; } @@ -29,42 +24,38 @@ const SingleCard = ({ address, price_min, price_max, - votes, + rating, onSelect, }: SingleCardProps) => ( - - - {' '} + + + {/* A clickable card with info about an apartment */} - onSelect({ id, name, address, price_min, price_max, votes }) + onSelect({ id, name, address, price_min, price_max, rating }) } > - - {/**/} -
+ + + {/**/} {name} {address} -
-
{/*review*/} @@ -72,7 +63,6 @@ const SingleCard = ({ ${price_min}-${price_max} @@ -80,16 +70,15 @@ const SingleCard = ({ {/* {rating} */} -
+
-
-
+ + ); export default SingleCard; diff --git a/src/frontend/src/components/Types.tsx b/src/frontend/src/components/Types.tsx index 77e3d7933..3d7373b8e 100644 --- a/src/frontend/src/components/Types.tsx +++ b/src/frontend/src/components/Types.tsx @@ -16,5 +16,13 @@ export type AptType = { address: string; price_min: number; price_max: number; - votes: number; + rating: number; +}; + +export type UserType = { + user_id: number; + username: string; + password: string; + email: string; + phone: string; }; diff --git a/src/frontend/src/components/mainpageleft/PopulateLeftSection.tsx b/src/frontend/src/components/mainpageleft/PopulateLeftSection.tsx index fb0f92c0e..2fa94f657 100644 --- a/src/frontend/src/components/mainpageleft/PopulateLeftSection.tsx +++ b/src/frontend/src/components/mainpageleft/PopulateLeftSection.tsx @@ -1,4 +1,15 @@ -import { Grid, ToggleButton, ToggleButtonGroup } from '@mui/material'; +import { + Grid, + Paper, + FormControl, + Select, + MenuItem, + SelectChangeEvent, + InputLabel, + Stack, + Typography, + Box, +} from '@mui/material'; import React, { useState, useRef, useCallback } from 'react'; import SingleCard from '../SingleCard'; import { useSearchParams } from 'react-router-dom'; @@ -44,43 +55,37 @@ export default function Populate({ onSelect }: Props) { [loading, hasMore] ); - const handlePriceToggle = ( - event: React.SyntheticEvent, - selected: string - ) => { - setPriceSort(selected); + const handlePriceToggle = (event: SelectChangeEvent) => { + setPriceSort(event.target.value); setId(-1); // start at the beginning // sets URL - if (selected === 'low-high') { + if (event.target.value === 'low-high') { searchParams.set('priceSort', '-1'); - } else if (selected === 'high-low') { + } else if (event.target.value === 'high-low') { searchParams.set('priceSort', '1'); } else { searchParams.delete('priceSort'); } searchParams.set('populate', 'True'); - if (selected) { + if (event.target.value) { searchParams.set('populate', 'False'); } setSearchParams(searchParams); }; - const handlePopularToggle = ( - event: React.SyntheticEvent, - selected: string - ) => { - setRatingSort(selected); + const handlePopularToggle = (event: SelectChangeEvent) => { + setRatingSort(event.target.value); setId(-1); // start at the beginning // sets URL searchParams.delete('aptId'); - if (selected === 'most popular') { + if (event.target.value === 'most popular') { searchParams.set('ratingSort', '1'); - } else if (selected === 'least popular') { + } else if (event.target.value === 'least popular') { searchParams.set('ratingSort', '-1'); } else { searchParams.delete('ratingSort'); } - if (selected) { + if (event.target.value) { searchParams.set('populate', 'True'); searchParams.set('numApts', '10'); } else { @@ -90,40 +95,54 @@ export default function Populate({ onSelect }: Props) { }; return ( - <> -
-
-
- - Low-High - High-Low - - - - Least Popular - - Most Popular - -
-
-
-
-
- {apartments.length === 0 && !loading && 'None found'} -
- + + + + {/* A paper UI with 2 drop down buttons for sorting */} + + + + {/* Sort by price */} + Price + + + + + + {/* Sort by popularity */} + Popularity + + + + + + {apartments.length === 0 && !loading && ( + + None found + + )} + + {/* A column of apartment info */} {apartments.map((apartment, i) => { if (apartments.length === i + 1) { return ( @@ -148,10 +167,18 @@ export default function Populate({ onSelect }: Props) { ); } })} - -
{loading && 'Loading...'}
-
{error && 'Error...'}
-
- + + {loading && ( + + Loading... + + )} + {error && ( + + Error... + + )} + + ); } diff --git a/src/frontend/src/components/mainpageleft/getApts.tsx b/src/frontend/src/components/mainpageleft/getApts.tsx index ee7192224..927766e95 100644 --- a/src/frontend/src/components/mainpageleft/getApts.tsx +++ b/src/frontend/src/components/mainpageleft/getApts.tsx @@ -44,7 +44,7 @@ function getApartments(priceSort: string, ratingSort: string, id: number) { address: res.data[i].address, price_min: res.data[i].price_min, price_max: res.data[i].price_max, - votes: res.data[i].votes, + rating: res.data[i].rating, }); } } diff --git a/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx b/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx index 59906234e..2c1a36b52 100644 --- a/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx +++ b/src/frontend/src/components/mainpageleft/getSearchBarSuggestions.tsx @@ -20,6 +20,7 @@ export default function getSuggestions(query: string, search: boolean) { method: 'GET', url: `http://127.0.0.1:5000/?search=${search}&searchQuery=${query}`, cancelToken: source.token, + withCredentials: true, }) .then((res) => { const newNames: { diff --git a/src/frontend/src/components/mainpageright/AddReview.tsx b/src/frontend/src/components/mainpageright/AddReview.tsx index 99a0448b7..2569632e8 100644 --- a/src/frontend/src/components/mainpageright/AddReview.tsx +++ b/src/frontend/src/components/mainpageright/AddReview.tsx @@ -1,19 +1,36 @@ -import React, { useState } from 'react'; -import { Button, Form } from 'react-bootstrap'; +import React, { useState, Dispatch, SetStateAction } from 'react'; +import { ReviewType, AptType } from '../Types'; import axios from 'axios'; +import { + TextField, + FormControl, + FormControlLabel, + FormLabel, + RadioGroup, + Button, + Radio, + Stack, +} from '@mui/material'; + +interface Props { + apt: AptType | undefined; + setReviews: Dispatch>; + username: string; +} const baseURL = 'http://127.0.0.1:5000/main'; -export default function AddReview() { +export default function AddReview({ apt, setReviews, username }: Props) { const [text, setText] = useState(''); const [vote, setVote] = useState(0); const addReviewHandler = async (text: string, vote: number) => { // post review on submit const result = await axios.post(`${baseURL}`, { - apt_id: 2, - username: 'Zongxian', + apt_id: apt?.id, + username: username, comment: text, vote: vote, }); + setReviews(result.data); console.log(result); }; const radioHandler = (event: React.ChangeEvent) => { @@ -34,59 +51,32 @@ export default function AddReview() { addReviewHandler(text, vote); }; return ( -
+ {/* {error && {error}} */} -
-
- - Create a Review - + + Create a review + setText(e.target.value)} - /> - -
-
- + + } + label="Upvote" /> - -
-
- } + label="Downvote" /> - -
-
- -
-
+ + + + + ); } diff --git a/src/frontend/src/components/mainpageright/AptInfo.tsx b/src/frontend/src/components/mainpageright/AptInfo.tsx index 2a6e89a76..e0131a033 100644 --- a/src/frontend/src/components/mainpageright/AptInfo.tsx +++ b/src/frontend/src/components/mainpageright/AptInfo.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; -import { Container } from 'react-bootstrap'; import { AptType } from '../Types'; +import { Stack, Box, Typography, Paper } from '@mui/material'; export interface IAptInfoProps { apt: AptType | undefined; @@ -8,41 +8,25 @@ export interface IAptInfoProps { export function AptInfo({ apt }: IAptInfoProps) { return ( - -

{apt?.name}

-
{apt?.address}
-
- Price Range: ${apt?.price_min}~${apt?.price_max} -
-
Rating: ${apt?.votes || 0}
- {/*
- - - - - {apt[0].upvotes} - - - - - - {apt[0].downvotes} - -
*/} -
+ + + + + {apt?.name} + + + {apt?.address} + + + + Price Range: ${apt?.price_min}~${apt?.price_max} + + + + Rating: {apt?.rating} + + + + ); } diff --git a/src/frontend/src/components/mainpageright/ImagesGallery.tsx b/src/frontend/src/components/mainpageright/ImagesGallery.tsx index 4e4d10035..e03fabe32 100644 --- a/src/frontend/src/components/mainpageright/ImagesGallery.tsx +++ b/src/frontend/src/components/mainpageright/ImagesGallery.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import 'bootstrap/dist/css/bootstrap.css'; -// import { PicType } from './Types'; -import Carousel from 'react-bootstrap/Carousel'; +import { Card, CardMedia } from '@mui/material'; +import Carousel from 'react-material-ui-carousel'; export interface IImagesGalleryProps { pics: string[]; @@ -9,13 +9,19 @@ export interface IImagesGalleryProps { const ImagesGallery = ({ pics }: IImagesGalleryProps) => { return ( - - {pics.map((pic) => ( - - - - ))} - + + + {pics.map((pic) => ( + + + + ))} + + ); }; diff --git a/src/frontend/src/components/mainpageright/ReviewCard.tsx b/src/frontend/src/components/mainpageright/ReviewCard.tsx index 035e65b31..6f8be3184 100644 --- a/src/frontend/src/components/mainpageright/ReviewCard.tsx +++ b/src/frontend/src/components/mainpageright/ReviewCard.tsx @@ -1,6 +1,17 @@ import React from 'react'; import 'bootstrap/dist/css/bootstrap.css'; // import { cp } from 'fs'; +import { + Card, + CardContent, + Avatar, + CardHeader, + Typography, + Stack, +} from '@mui/material'; +import PersonIcon from '@mui/icons-material/Person'; +import ThumbUpIcon from '@mui/icons-material/ThumbUp'; +import ThumbDownIcon from '@mui/icons-material/ThumbDown'; interface ReviewCardProps { username: string; date: string; @@ -8,45 +19,35 @@ interface ReviewCardProps { vote: boolean; } const ReviewCard = ({ username, date, comment, vote }: ReviewCardProps) => { - if (vote === true) { - return ( -
-

{username}

-

{date}

-
{comment}
-
- - - -
-
- ); - } return ( -
-

{username}

-

{date}

-
{comment}
-
- - - -
-
+ + + + + + } + title={username} + subheader={date} + /> + + + {comment} + {vote === true && ( + + + + )} + {vote !== true && ( + + + + )} + + + + ); }; diff --git a/src/frontend/src/components/mainpageright/ReviewsList.tsx b/src/frontend/src/components/mainpageright/ReviewsList.tsx index 2fa75b983..8fed6b67f 100644 --- a/src/frontend/src/components/mainpageright/ReviewsList.tsx +++ b/src/frontend/src/components/mainpageright/ReviewsList.tsx @@ -1,34 +1,38 @@ import React from 'react'; import ReviewCard from './ReviewCard'; import { ReviewType } from '../Types'; - +import { Box, Typography, Stack } from '@mui/material'; interface Props { reviews: ReviewType[]; } const ReviewsList = ({ reviews }: Props) => { - if (reviews.length === 0) { - return ( -
-

No comment yet. Write your first comment

-
- ); - } else { - return ( -
-

Comments

-
- {reviews.map((review, i) => ( - - ))} -
- ); - } + return ( + + {reviews.length === 0 && ( + + + No comment yet. Write your first comment + + + )} + {reviews.length !== 0 && ( + + + Comments + + {reviews.map((review, i) => ( + + ))} + + )} + + ); }; export default ReviewsList; diff --git a/src/frontend/src/components/user/FormEmail.tsx b/src/frontend/src/components/user/FormEmail.tsx new file mode 100644 index 000000000..c1feda832 --- /dev/null +++ b/src/frontend/src/components/user/FormEmail.tsx @@ -0,0 +1,132 @@ +import axios from 'axios'; +import { + Grid, + Typography, + ListItemText, + Box, + Stack, + Button, + ListItemAvatar, + Avatar, + ListItem, + TextField, +} from '@mui/material'; +import React, { useState, Dispatch, SetStateAction } from 'react'; +import EmailIcon from '@mui/icons-material/Email'; + +interface EmailComponentProps { + displayEmail: string; + setDisplayEmail: Dispatch>; +} +const baseURL = 'http://127.0.0.1:5000/user'; + +export function FormEmail({ + displayEmail, + setDisplayEmail, +}: EmailComponentProps) { + const [editEmail, setEditEmail] = useState(false); + const [newEmail, setNewEmail] = useState(''); + const [success, setSuccess] = useState(true); + + function changeEmail(new_email: string) { + const req = { + is_email: true, + email: new_email, + }; + const json = JSON.stringify(req); + axios({ + method: 'POST', + url: `${baseURL}`, + data: json, + withCredentials: true, + }) + .then((response) => { + console.log(response); + setDisplayEmail(newEmail); + setEditEmail(false); + setSuccess(true); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + setSuccess(false); + } + }); + } + + return ( + + {/* Email box changes based on click */} + {editEmail === false && ( + + + + + + + + + + )} + {editEmail === true && ( + + + + + { + setNewEmail(event.target.value); + }} + /> + + + + + + + + + + + + + {success === false && ( + + Invalid Email + + )} + + + )} + + ); +} diff --git a/src/frontend/src/components/user/FormLikedApts.tsx b/src/frontend/src/components/user/FormLikedApts.tsx new file mode 100644 index 000000000..574a3fe49 --- /dev/null +++ b/src/frontend/src/components/user/FormLikedApts.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Box, Stack, Button } from '@mui/material'; +import getReviewedApts from './getReviewedApts'; + +interface LikedAptsProps { + id: number; +} + +export function FormLikedApts({ id }: LikedAptsProps) { + console.log('Getting apt info'); + const reviewed_apts = getReviewedApts(id); + return ( + + {/* UI for liked apartments */} + + + {reviewed_apts.apartments.map((apt, i) => { + return ( + + ); + })} + + + + ); +} diff --git a/src/frontend/src/components/user/FormPhone.tsx b/src/frontend/src/components/user/FormPhone.tsx new file mode 100644 index 000000000..4f7a9cf66 --- /dev/null +++ b/src/frontend/src/components/user/FormPhone.tsx @@ -0,0 +1,132 @@ +import axios from 'axios'; +import { + Grid, + Typography, + ListItemText, + Box, + Stack, + Button, + ListItemAvatar, + Avatar, + ListItem, + TextField, +} from '@mui/material'; +import React, { useState, Dispatch, SetStateAction } from 'react'; +import PhoneIcon from '@mui/icons-material/Phone'; + +const baseURL = 'http://127.0.0.1:5000/user'; +interface PhoneComponentProps { + displayPhone: string; + setDisplayPhone: Dispatch>; +} + +export function FormPhone({ + displayPhone, + setDisplayPhone, +}: PhoneComponentProps) { + const [editPhone, setEditPhone] = useState(false); + const [newPhone, setNewPhone] = useState(''); + const [success, setSuccess] = useState(true); + + function changePhone(new_phone: string) { + const req = { + is_phone: true, + phone: new_phone, + }; + const json = JSON.stringify(req); + axios({ + method: 'POST', + url: `${baseURL}`, + data: json, + withCredentials: true, + }) + .then((response) => { + console.log(response); + setDisplayPhone(newPhone); + setEditPhone(false); + setSuccess(true); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + setSuccess(false); + } + }); + } + + return ( + + {/* Phone box changes based on click */} + {editPhone === false && ( + + + + + + + + + + )} + {editPhone === true && ( + + + + + { + setNewPhone(event.target.value); + }} + /> + + + + + + + + + + + + + {success === false && ( + + Invalid phone number + + )} + + + )} + + ); +} diff --git a/src/frontend/src/components/user/FormUser.tsx b/src/frontend/src/components/user/FormUser.tsx new file mode 100644 index 000000000..a49099188 --- /dev/null +++ b/src/frontend/src/components/user/FormUser.tsx @@ -0,0 +1,57 @@ +import React, { useEffect } from 'react'; +import { + List, + ListItemText, + Divider, + ListItemAvatar, + Avatar, + ListItem, +} from '@mui/material'; +import PersonIcon from '@mui/icons-material/Person'; +import getInfo from './getUser'; +import { FormEmail } from './FormEmail'; +import { FormPhone } from './FormPhone'; +import { useState, Dispatch, SetStateAction } from 'react'; + +interface UserProps { + setId: Dispatch>; +} + +export function FormUser({ setId }: UserProps) { + const user_info = getInfo(); + const [displayEmail, setDisplayEmail] = useState(''); + const [displayPhone, setDisplayPhone] = useState(''); + useEffect(() => { + setDisplayEmail(user_info.user.email); + setDisplayPhone(user_info.user.phone); + setId(user_info.user.user_id); + }, [user_info.user.email, user_info.user.phone, user_info.user.user_id]); + return ( + + {/* Form UI for user info */} + + + + + + + + + + + + + + + + ); +} diff --git a/src/frontend/src/components/user/LogOut.tsx b/src/frontend/src/components/user/LogOut.tsx new file mode 100644 index 000000000..a4e4580dc --- /dev/null +++ b/src/frontend/src/components/user/LogOut.tsx @@ -0,0 +1,18 @@ +import axios from 'axios'; + +export function logout() { + axios({ + url: 'http://127.0.0.1:5000/logout', + withCredentials: true, + }) + .then((response) => { + console.log(response); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + } + }); +} diff --git a/src/frontend/src/components/user/getReviewedApts.tsx b/src/frontend/src/components/user/getReviewedApts.tsx new file mode 100644 index 000000000..6955efa3c --- /dev/null +++ b/src/frontend/src/components/user/getReviewedApts.tsx @@ -0,0 +1,62 @@ +import { useEffect, useState } from 'react'; +import axios from 'axios'; +import { AptType } from '../Types'; + +const baseURL = 'http://127.0.0.1:5000/user'; + +export default function getReviewedApts(id: number) { + // Get apts that username reviewed + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const emptyarray: AptType[] = []; + const [apartments, setApartments] = useState(emptyarray); + + useEffect(() => { + setLoading(true); + setError(false); + const CancelToken = axios.CancelToken; + const source = CancelToken.source(); + const timer = setTimeout(() => { + const req = { + is_get_liked: true, + user_id: id, + }; + const json = JSON.stringify(req); + axios({ + method: 'POST', + url: `${baseURL}`, + data: json, + cancelToken: source.token, + withCredentials: true, + }) + .then((res) => { + setApartments(() => { + const newApartments: AptType[] = []; + for (let i = 0; i < res.data.length; i++) { + if (res.data[i].name !== undefined) { + newApartments.push({ + // this is necessary; pushing res.data[i] does not work + id: res.data[i].apt_id, + name: res.data[i].name, + address: res.data[i].address, + price_min: res.data[i].price_min, + price_max: res.data[i].price_max, + rating: res.data[i].rating, + }); + } + } + return newApartments; + }); + }) + .catch((e) => { + if (axios.isCancel(e)) return; + setError(true); + }); + }, 100); + return () => { + clearTimeout(timer); + source.cancel(); + }; + }, [id]); + return { loading, error, apartments }; +} diff --git a/src/frontend/src/components/user/getUser.tsx b/src/frontend/src/components/user/getUser.tsx new file mode 100644 index 000000000..ad5f79056 --- /dev/null +++ b/src/frontend/src/components/user/getUser.tsx @@ -0,0 +1,58 @@ +import { useEffect, useState } from 'react'; +import axios from 'axios'; +import { UserType } from '../Types'; + +const baseURL = 'http://127.0.0.1:5000/user'; + +export default function getInfo() { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const [user, setUser] = useState(defaultUser); + + useEffect(() => { + //Gets user info on load + setLoading(true); + setError(false); + const CancelToken = axios.CancelToken; + const source = CancelToken.source(); + const timer = setTimeout(() => { + axios({ + method: 'GET', + url: `${baseURL}`, + cancelToken: source.token, + withCredentials: true, + }) + .then((res) => { + setUser(() => { + return { + user_id: res.data.user_id, + username: res.data.username, + password: res.data.password, + email: res.data.email, + phone: res.data.phone, + }; + }); + }) + .catch((e) => { + if (axios.isCancel(e)) return; + setError(true); + }); + }, 100); + return () => { + clearTimeout(timer); + source.cancel(); + }; + }, []); + return { loading, error, user }; +} + +function defaultUser(): UserType { + // Default user for when page loads + return { + user_id: 0, + username: '', + password: '', + email: '', + phone: '', + }; +} diff --git a/src/frontend/src/pages/Login.tsx b/src/frontend/src/pages/Login.tsx index a09c7c34f..3f939c93f 100644 --- a/src/frontend/src/pages/Login.tsx +++ b/src/frontend/src/pages/Login.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Grid, Paper, @@ -8,83 +8,111 @@ import { Link, FormControlLabel, Checkbox, + Stack, } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import axios from 'axios'; import { useState } from 'react'; - -function sendData(user: string, password: string) { - axios({ - method: 'post', - url: '/login', - data: { - user: user, - password: password, - }, - }) - .then((response) => { - console.log(response); - }) - .catch((error) => { - if (error.response) { - console.log(error.response); - console.log(error.response.status); - console.log(error.response.headers); - } - }); -} +import { useNavigate } from 'react-router-dom'; export default function Login() { + const navigate = useNavigate(); const [user, setUser] = useState(''); const [password, setPassword] = useState(''); + const [res, setRes] = useState(); + + function sendData() { + axios({ + method: 'post', + url: 'http://127.0.0.1:5000/login', + withCredentials: true, + data: { + user: user, + password: password, + }, + }) + .then((response) => { + console.log(response); + setRes(response.data); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + setRes(error.response.data); + } + }); + } const paperStyle = { padding: 20, - height: '55vh', + height: '65vh', width: 310, margin: '20px auto', }; + useEffect(() => { + if (res === `welcome ${user}`) { + navigate('/'); + } + }, [res, user]); + const btnstyle = { margin: '8px 0' }; return ( - -

Sign In

- setUser(event.target.value)} - fullWidth - required - /> - setPassword(event.target.value)} - fullWidth - required - /> - } - label="Remember me" - /> - + + {/* A paper like UI with fields for login */} + + + + + +

Sign In

+
+
+ setUser(event.target.value)} + fullWidth + required + /> + setPassword(event.target.value)} + fullWidth + required + /> + } + label="Remember me" + /> + +
Forgot Password - Sign Up + Sign Up + + + Access without logging in + {res !== undefined && res !== `welcome ${user}` && ( + {res} + )}
); diff --git a/src/frontend/src/pages/MainPage.tsx b/src/frontend/src/pages/MainPage.tsx index 45afd93b9..36dbfb3a2 100644 --- a/src/frontend/src/pages/MainPage.tsx +++ b/src/frontend/src/pages/MainPage.tsx @@ -4,28 +4,143 @@ import Populate from '../components/mainpageleft/PopulateLeftSection'; import SearchBar from '../components/SearchBar'; import { AptType } from '../components/Types'; import RightSection from '../sections/MainPageRightSection'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import { logout } from '../components/user/LogOut'; +import { useNavigate } from 'react-router-dom'; +import { + Stack, + AppBar, + Toolbar, + Grid, + Box, + Avatar, + Button, +} from '@mui/material'; +import axios from 'axios'; function MainPage() { + const navigate = useNavigate(); const { apartments } = getApartments('0', '0', -1); const [to, setTo] = useState(apartments[0]); + const [logged, setLogged] = useState(false); + const [username, setUsername] = useState(''); + function checkLoggedIn() { + axios({ + url: 'http://127.0.0.1:5000/api/whoami', + withCredentials: true, + }) + .then((response) => { + console.log(response); + setLogged(true); + setUsername(response.data); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + } + }); + } + checkLoggedIn(); + return ( <> -
- -
-
-
-
- -
-
+ + {/* Top bar */} + + + + + + + + + + + {logged === true && ( + + + + + + )} + + + + + + {logged === true && ( + + + + + + )} + {logged === false && ( + + + + + + )} + + + + + {/* Search bar*/} + + + + + + + + {/* The rest of the components */} + setTo(apt)} /> -
-
-
+ + + + + + ); } diff --git a/src/frontend/src/pages/Register.tsx b/src/frontend/src/pages/Register.tsx index ffaa93671..8350cdd6d 100644 --- a/src/frontend/src/pages/Register.tsx +++ b/src/frontend/src/pages/Register.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Grid, Paper, @@ -8,103 +8,130 @@ import { Link, FormControlLabel, Checkbox, + Stack, } from '@mui/material'; import PersonIcon from '@mui/icons-material/Person'; import axios from 'axios'; - -function sendData( - username: string, - email: string, - password: string, - phone: string -) { - axios({ - method: 'post', - url: '/register', - data: { - username: username, - email: email, - password: password, - phone: phone, - }, - }) - .then((response) => { - console.log(response); - }) - .catch((error) => { - if (error.response) { - console.log(error.response); - console.log(error.response.status); - console.log(error.response.headers); - } - }); -} +import { useNavigate } from 'react-router-dom'; export default function Register() { const [user, setUser] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [number, setNumber] = useState(''); - + const [res, setRes] = useState(); const paperStyle = { padding: 20, - height: '55vh', + height: '80vh', width: 310, margin: '20px auto', }; const btnstyle = { margin: '8px 0' }; + function sendData() { + axios({ + method: 'POST', + url: 'http://127.0.0.1:5000/register', + withCredentials: true, + data: { + username: user, + email: email, + password: password, + phone: number, + }, + }) + .then((response) => { + console.log(response); + setRes(response.data); + }) + .catch((error) => { + if (error.response) { + console.log(error.response); + console.log(error.response.status); + console.log(error.response.headers); + setRes(error.response.data); + } + }); + } + const navigate = useNavigate(); + + useEffect(() => { + if (res === `Register successful, welcome ${user}`) { + setTimeout(() => { + navigate('/login'); + }, 2500); + } + }, [res, user, navigate]); + return ( - -

Register

- setUser(event.target.value)} - fullWidth - required - /> - setEmail(event.target.value)} - fullWidth - required - /> - setPassword(event.target.value)} - type="password" - fullWidth - required - /> - setNumber(event.target.value)} - fullWidth - required - /> - } - label="Remember me" - /> - + + {/* A paper like UI with fields for register*/} + + + + + +

Register

+
+
+ setUser(event.target.value)} + fullWidth + required + /> + setEmail(event.target.value)} + fullWidth + required + /> + setPassword(event.target.value)} + type="password" + fullWidth + required + /> + setNumber(event.target.value)} + fullWidth + required + /> + } + label="Remember me" + /> + +
- Already signed up? + Already signed up? + {res === `Register successful, welcome ${user}` && ( + + {res + '. You will be directed to the login page shortly.'} + + )} + {res !== undefined && + res !== `Register successful, welcome ${user}` && ( + {res} + )}
); diff --git a/src/frontend/src/pages/User.tsx b/src/frontend/src/pages/User.tsx new file mode 100644 index 000000000..2674d9db1 --- /dev/null +++ b/src/frontend/src/pages/User.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { + Grid, + Typography, + Box, + Stack, + Button, + Avatar, + AppBar, + Toolbar, +} from '@mui/material'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import { logout } from '../components/user/LogOut'; +import { FormUser } from '../components/user/FormUser'; +import { FormLikedApts } from '../components/user/FormLikedApts'; +import { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; + +export default function User() { + const [id, setId] = useState(-1); + const navigate = useNavigate(); + const btnstyle = { marginLeft: '10px' }; + return ( + <> + + + {/*Renders top bar*/} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/*Headers*/} + + + User + + + + + Reviewed Apartments + + + + + {/* User info and list of reviewed apts */} + + + + + + + + + + + + ); +} diff --git a/src/frontend/src/pages/UserPage.tsx b/src/frontend/src/pages/UserPage.tsx deleted file mode 100644 index e5addc659..000000000 --- a/src/frontend/src/pages/UserPage.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -function UserPage() { - return ( - <> -
- - ); -} -export default UserPage; diff --git a/src/frontend/src/sections/MainPageRightSection.tsx b/src/frontend/src/sections/MainPageRightSection.tsx index 996b03a8f..2061dcaff 100644 --- a/src/frontend/src/sections/MainPageRightSection.tsx +++ b/src/frontend/src/sections/MainPageRightSection.tsx @@ -5,12 +5,15 @@ import ImagesGallery from '../components/mainpageright/ImagesGallery'; import { ReviewType, AptType } from '../components/Types'; import { AptInfo } from '../components/mainpageright/AptInfo'; import axios from 'axios'; +import { Stack, Divider } from '@mui/material'; const baseURL = 'http://127.0.0.1:5000/main'; interface apt { apt: AptType | undefined; // in case of null + logged: boolean; + username: string; } -function RightSection({ apt }: apt) { +function RightSection({ apt, logged, username }: apt) { const [reviews, setReviews] = useState([]); const [pics, setPics] = useState([ 'https://www.salonlfc.com/wp-content/uploads/2018/01/image-not-found-scaled.png', @@ -51,12 +54,27 @@ function RightSection({ apt }: apt) { getAllReviews(); }, [apt]); return ( -
- - - - -
+ + {/* A column of every element on the right half */} + + + + {logged === true && ( + + )} + {logged === true && ( + + )} + + + + ); } diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 9ece75d75..a36c4781b 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -1229,6 +1229,13 @@ "resolved" "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz" "version" "0.9.0" +"@emotion/is-prop-valid@^0.8.2": + "integrity" "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==" + "resolved" "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz" + "version" "0.8.8" + dependencies: + "@emotion/memoize" "0.7.4" + "@emotion/is-prop-valid@^1.1.0", "@emotion/is-prop-valid@^1.2.0": "integrity" "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==" "resolved" "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz" @@ -1241,7 +1248,12 @@ "resolved" "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz" "version" "0.8.0" -"@emotion/react@^11.10.4": +"@emotion/memoize@0.7.4": + "integrity" "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + "resolved" "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz" + "version" "0.7.4" + +"@emotion/react@^11.10.4", "@emotion/react@^11.7.1": "integrity" "sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==" "resolved" "https://registry.npmjs.org/@emotion/react/-/react-11.10.4.tgz" "version" "11.10.4" @@ -1271,7 +1283,7 @@ "resolved" "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.0.tgz" "version" "1.2.0" -"@emotion/styled@^11.10.4": +"@emotion/styled@^11.10.4", "@emotion/styled@^11.6.0": "integrity" "sha512-pRl4R8Ez3UXvOPfc2bzIoV8u9P97UedgHS4FPX594ntwEuAMA114wlaHvOK24HB48uqfXiGlYIZYCxVJ1R1ttQ==" "resolved" "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.4.tgz" "version" "11.10.4" @@ -1718,14 +1730,14 @@ "resolved" "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.10.8.tgz" "version" "5.10.8" -"@mui/icons-material@^5.10.6": +"@mui/icons-material@^5.10.6", "@mui/icons-material@^5.4.1": "integrity" "sha512-QwxdRmLA46S94B0hExPDx0td+A2unF+33bQ6Cs+lNpJKVsm1YeHwNdYXYcnpWeHeQQ07055OXl7IB2GKDd0MfA==" "resolved" "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.10.6.tgz" "version" "5.10.6" dependencies: "@babel/runtime" "^7.19.0" -"@mui/material@^5.10.7": +"@mui/material@^5.10.7", "@mui/material@^5.4.1": "integrity" "sha512-sF/Ka0IJjGXV52zoT4xAWEqXVRjNYbIjATo9L4Q5oQC5iJpGrKJFY16uNtWWB0+vp/nayAuPGZHrxtV+t3ecdQ==" "resolved" "https://registry.npmjs.org/@mui/material/-/material-5.10.8.tgz" "version" "5.10.8" @@ -1762,7 +1774,7 @@ "csstype" "^3.1.1" "prop-types" "^15.8.1" -"@mui/system@^5.10.8": +"@mui/system@^5.10.8", "@mui/system@^5.4.1": "integrity" "sha512-hRQ354zcrYP/KHqK8FheICSvE9raQaUgQaV+A3oD4JETaFUCVI9Ytt+RcQYgTqx02xlCXIjl8LK1rPjTneySqw==" "resolved" "https://registry.npmjs.org/@mui/system/-/system-5.10.8.tgz" "version" "5.10.8" @@ -4977,6 +4989,26 @@ "resolved" "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" "version" "4.2.0" +"framer-motion@^4.1.17": + "integrity" "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==" + "resolved" "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz" + "version" "4.1.17" + dependencies: + "framesync" "5.3.0" + "hey-listen" "^1.0.8" + "popmotion" "9.3.6" + "style-value-types" "4.1.4" + "tslib" "^2.1.0" + optionalDependencies: + "@emotion/is-prop-valid" "^0.8.2" + +"framesync@5.3.0": + "integrity" "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==" + "resolved" "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz" + "version" "5.3.0" + dependencies: + "tslib" "^2.1.0" + "fresh@0.5.2": "integrity" "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" "resolved" "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" @@ -5021,6 +5053,11 @@ "resolved" "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" "version" "1.0.0" +"fsevents@^2.3.2", "fsevents@~2.3.2": + "integrity" "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==" + "resolved" "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + "version" "2.3.2" + "function-bind@^1.1.1": "integrity" "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "resolved" "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" @@ -5234,6 +5271,11 @@ "resolved" "https://registry.npmjs.org/he/-/he-1.2.0.tgz" "version" "1.2.0" +"hey-listen@^1.0.8": + "integrity" "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" + "resolved" "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz" + "version" "1.0.8" + "hoist-non-react-statics@^3.0.0", "hoist-non-react-statics@^3.3.0", "hoist-non-react-statics@^3.3.1", "hoist-non-react-statics@^3.3.2": "integrity" "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==" "resolved" "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" @@ -7151,6 +7193,16 @@ dependencies: "find-up" "^3.0.0" +"popmotion@9.3.6": + "integrity" "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==" + "resolved" "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz" + "version" "9.3.6" + dependencies: + "framesync" "5.3.0" + "hey-listen" "^1.0.8" + "style-value-types" "4.1.4" + "tslib" "^2.1.0" + "popper.js@1.16.1-lts": "integrity" "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" "resolved" "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz" @@ -7986,6 +8038,18 @@ "resolved" "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz" "version" "3.0.4" +"react-material-ui-carousel@3.4.2": + "integrity" "sha512-jUbC5aBWqbbbUOOdUe3zTVf4kMiZFwKJqwhxzHgBfklaXQbSopis4iWAHvEOLcZtSIJk4JAGxKE0CmxDoxvUuw==" + "resolved" "https://registry.npmjs.org/react-material-ui-carousel/-/react-material-ui-carousel-3.4.2.tgz" + "version" "3.4.2" + dependencies: + "@emotion/react" "^11.7.1" + "@emotion/styled" "^11.6.0" + "@mui/icons-material" "^5.4.1" + "@mui/material" "^5.4.1" + "@mui/system" "^5.4.1" + "framer-motion" "^4.1.17" + "react-refresh@^0.11.0": "integrity" "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==" "resolved" "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" @@ -8860,6 +8924,14 @@ "resolved" "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz" "version" "3.3.1" +"style-value-types@4.1.4": + "integrity" "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==" + "resolved" "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz" + "version" "4.1.4" + dependencies: + "hey-listen" "^1.0.8" + "tslib" "^2.1.0" + "styled-components@^5.3.6": "integrity" "sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==" "resolved" "https://registry.npmjs.org/styled-components/-/styled-components-5.3.6.tgz" @@ -9155,7 +9227,7 @@ "resolved" "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" "version" "1.14.1" -"tslib@^2.0.3": +"tslib@^2.0.3", "tslib@^2.1.0": "integrity" "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" "resolved" "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz" "version" "2.4.0"