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)}
- />
-
-
-
-
-
+
+
+
+
+
);
}
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"