Compare commits

...

57 Commits

Author SHA1 Message Date
a56621d6ae Add README 2023-04-14 21:24:36 -04:00
3bbeed57ed #58 add admin logic 2023-04-14 19:03:54 -04:00
97e958697f #76 Added course content page 2023-04-14 18:31:04 -04:00
035ab9a6c7 #76 Made content widget 2023-04-14 18:13:59 -04:00
b1008a63d9 #66 Add edit button that takes user to assignment edit page 2023-04-14 17:58:09 -04:00
dccc1949e0 #47 Made manage content page 2023-04-14 17:06:25 -04:00
8626fd9a44 #70 Add endpoint to update content 2023-04-14 17:02:01 -04:00
cab68b147c #70 Add endpoint to delete content 2023-04-14 17:00:10 -04:00
0ea4e3933f #70 Add endpoint to get content by id 2023-04-14 16:57:58 -04:00
608854ebb0 #70 Add endpoint to get all content in a specific course 2023-04-14 16:56:25 -04:00
245b7bbb2a #70 Add endpoint to create content 2023-04-14 16:54:34 -04:00
80ccc17eef #70 Add content model 2023-04-14 16:47:27 -04:00
b03c50445f #67 Made endpoint to update assignment fields 2023-04-14 16:37:04 -04:00
35ddecb5a9 #65 endpoint to delete assignment 2023-04-14 16:12:52 -04:00
35e8b2eec3 #46 Manage assignments page shows table of assignments for a specific course. 2023-04-14 16:06:47 -04:00
ecbbcb04c0 #62 Made assignment page, showing name, description, and due date 2023-04-14 15:11:27 -04:00
ddf8fd57a0 #61 Added assignment widget on course page 2023-04-14 14:59:14 -04:00
9be7abcd05 #53 Endpoint to get an assignment by id 2023-04-14 14:48:14 -04:00
27d2317e84 #57 CORS allow production url 2023-04-14 14:42:30 -04:00
0740f1f4ac #57 Use production env for docker 2023-04-14 14:30:53 -04:00
f513c31664 #57 Production backend url 2023-04-14 14:20:15 -04:00
c7797c9401 #57 Copy venv from container's cwd, not host 2023-04-14 14:02:10 -04:00
275f0f0914 #57 Added frontend env files for local and production. 2023-04-14 13:55:34 -04:00
c1a8f04dc7 #57 makeRequest uses env variable so all requests use the configuration 2023-04-14 12:55:43 -04:00
1f0e8ca905 #52 Create endpoint to get a course's assignments 2023-04-13 18:49:47 -04:00
389f71c2f8 #10 Made endpoint to create a new assignment 2023-04-13 18:46:54 -04:00
2b071d35c8 #10 Add due date column to assignment model 2023-04-13 18:35:51 -04:00
104370fb79 #10 Create assignment model 2023-04-13 18:32:09 -04:00
8670612259 Cleanup routes with AuthRoute 2023-04-13 18:18:30 -04:00
29e417c9d0 #45 Add student form, unenroll button 2023-04-13 18:07:06 -04:00
a6ae87abf2 #45 Added backend route to enroll student by username 2023-04-13 18:05:43 -04:00
4faecbd29c #45 Made manage student page. Doesn't call backend yet 2023-04-13 16:52:28 -04:00
542e8c232a #49 Add endpoint to get the students enrolled in a specific course 2023-04-13 16:45:39 -04:00
f0b101386b #44 Made manage page. Displays table of all courses that the instuctor is teaching 2023-04-13 16:11:33 -04:00
e07b5add79 #38 Made instructor specific links on the navbar 2023-04-13 15:02:41 -04:00
0db9844e77 Add instructor as an enrolled user to a course. Also change frontend to use instructor role instead of teacher 2023-04-13 14:51:40 -04:00
797b7235e9 Course widget now uses correct id field to navigate to course page 2023-04-13 14:38:04 -04:00
a2bc3567c5 #39 Added endpoint to get a specific course by id 2023-04-13 14:11:18 -04:00
24f6256703 #31 Added course page that querys backend for a specific course id 2023-04-13 14:10:26 -04:00
1dfa9c1421 #32 Remove dummy data. Modified makeRequest so it doesn't send body on GET 2023-04-06 23:10:38 -04:00
15d336ca53 #32 Fetch enrolled course data from backend and store the data 2023-04-06 23:10:38 -04:00
9c6b46f68f Add decorator that requirers the logged in user to be an instructor 2023-04-06 23:08:18 -04:00
d5d2d830cc #35 Added course code field to Course model. Updated class functions to reflect change. Also added duplicate checking for create course endpoint 2023-04-06 22:00:11 -04:00
dcb9111288 #6 Add mysql client requirement to requirements 2023-04-06 21:49:09 -04:00
a789c2eb83 #6 Refactored both functionalities to one function that accepts both request methods 2023-04-06 21:25:34 -04:00
b71c23d706 #6 Added unenroll endpoint by sending DELETE request to enroll endpoint 2023-04-06 19:06:44 -04:00
8ec489beb3 #6 Created route to enroll student into a course 2023-04-06 18:51:43 -04:00
0b392ab5b1 #11 Change route so that caller doesn't have to pass user_id through request body 2023-04-06 18:11:48 -04:00
84fb1e9f4b #11 Added endpoint to get enrolled courses 2023-04-06 16:40:56 -04:00
5beaf2dba4 #11 Added enrollment table and relationship between user/couse 2023-04-06 16:19:15 -04:00
12498c2ee8 #5 Create course endpoint. Added messages to all db migrations 2023-04-06 15:23:10 -04:00
20f0309c5c #5 Added course model 2023-04-06 15:00:10 -04:00
e4af6457b9 #28 Error handling for logout request 2023-04-06 14:40:17 -04:00
d1a35dd2c1 Changed widget layout. Also changed the dummy data 2023-03-23 16:41:46 -04:00
214b1c8657 #25 Moved sendLoginRequest and sendLogoutRequest to utils 2023-03-21 12:22:38 -04:00
b1f6d6b22c #24 Added courses widget with some dummy data 2023-03-20 22:17:52 -04:00
d05e0fb0e0 #21 Cleanup 2023-03-19 16:49:39 -04:00
42 changed files with 2050 additions and 111 deletions

45
README.md Normal file
View File

@@ -0,0 +1,45 @@
This project uses React on the frontend and Flask on the backend. The goal of this
project was to make a Brightspace clone/remake.
# Running the code
## Docker
If you have docker and docker-compose installed, you can simply use the provided
`docker-compose.yml` file.
```sh
echo "REACT_APP_BACKEND_URL=http://localhost:5000" > frontend/.env
docker-compose up -d --build
```
Once both services are up, head to [http://localhost:8080](http://localhost:8080)
**NOTE**: You might have to refresh the webpage a few times. For some reason it bugs out
sometimes
## Manual
### Backend setup
Run this in a new terminal
```sh
cd backend
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
flask run
```
### Frontend setup
Run this in a new terminal
```sh
cd frontend
npm install
npm start
```

View File

@@ -5,7 +5,7 @@ RUN python -m venv venv
COPY ./requirements.txt /code/requirements.txt COPY ./requirements.txt /code/requirements.txt
RUN . venv/bin/activate RUN . venv/bin/activate
RUN python -m pip install -r /code/requirements.txt RUN python -m pip install -r /code/requirements.txt
COPY venv /code/venv RUN cp -r venv /code/venv
WORKDIR /code WORKDIR /code
COPY app app COPY app app

View File

@@ -1,37 +1,147 @@
from app import db from app import db
import sqlalchemy as sa
from flask_login import UserMixin from flask_login import UserMixin
from datetime import datetime from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
class User(UserMixin, db.Model): enrollment = db.Table(
id = db.Column(db.Integer, primary_key=True) "enrollment",
username = db.Column(db.String(64), index=True, unique=True) sa.Column("user_id", sa.ForeignKey("user.id"), primary_key=True),
role = db.Column(db.String(32), index=True) sa.Column("course_id", sa.ForeignKey("course.id"), primary_key=True),
email = db.Column(db.String(120), index=True, unique=True) )
password_hash = db.Column(db.String(128))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
token = db.Column(db.String(32), index=True, unique=True)
def __repr__(self):
class User(UserMixin, db.Model):
id = sa.Column(sa.Integer, primary_key=True)
username = sa.Column(sa.String(64), index=True, unique=True)
role = sa.Column(sa.String(32), index=True)
email = sa.Column(sa.String(120), index=True, unique=True)
password_hash = sa.Column(sa.String(128))
last_seen = sa.Column(sa.DateTime, default=datetime.utcnow)
token = sa.Column(sa.String(32), index=True, unique=True)
enrolled_courses = db.relationship(
"Course",
secondary=enrollment,
backref=db.backref("students", lazy="dynamic"),
lazy="dynamic",
)
def __repr__(self) -> str:
return f"<User {self.username}>" return f"<User {self.username}>"
def set_password(self, password): def set_password(self, password) -> None:
self.password_hash = generate_password_hash(password) self.password_hash = generate_password_hash(password)
def check_password(self, password): def check_password(self, password) -> bool:
return check_password_hash(self.password_hash, password) return check_password_hash(self.password_hash, password)
def to_dict(self): def is_enrolled(self, c) -> bool:
return self.enrolled_courses.filter(enrollment.c.course_id == c.id).count() > 0
def enroll(self, c) -> bool:
if not self.is_enrolled(c):
self.enrolled_courses.append(c)
db.session.commit()
return True
return False
def unenroll(self, c) -> bool:
if self.is_enrolled(c):
self.enrolled_courses.remove(c)
db.session.commit()
return True
return False
def to_dict(self) -> dict:
return { return {
"id": self.id, "id": self.id,
"username": self.username, "username": self.username,
"email": self.email, "email": self.email,
"role": self.role,
} }
def from_dict(self, data, new_user=False): def from_dict(self, data, new_user=False) -> None:
for field in ["role", "username", "email"]: for field in ["role", "username", "email"]:
if field in data: if field in data:
setattr(self, field, data[field]) setattr(self, field, data[field])
if new_user and "password" in data: if new_user and "password" in data:
self.set_password(data["password"]) self.set_password(data["password"])
class Course(db.Model):
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(128), index=True)
course_code = sa.Column(sa.String(32), index=True)
description = sa.Column(sa.Text, index=True)
instructor = sa.Column(sa.ForeignKey(User.id), index=True)
created_at = sa.Column(sa.DateTime)
assignments = db.relationship("Assignment", backref="course", lazy="dynamic")
content = db.relationship("Content", backref="course", lazy="dynamic")
def __repr__(self) -> str:
return f"<Course {self.course_code}>"
def from_dict(self, data) -> None:
for field in ["name", "course_code", "description", "instructor"]:
if field in data:
setattr(self, field, data[field])
if not self.created_at:
self.created_at = datetime.now()
def to_dict(self) -> dict:
d = {}
for f in ["id", "name", "course_code", "description", "created_at"]:
d[f] = getattr(self, f)
d["instructor"] = User.query.get(self.instructor).username
return d
class Assignment(db.Model):
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(128), index=True)
course_id = sa.Column(sa.ForeignKey(Course.id), index=True)
description = sa.Column(sa.Text, index=True)
due_date = sa.Column(sa.DateTime)
created_at = sa.Column(sa.DateTime)
def from_dict(self, data) -> None:
for field in ["name", "course_id", "description", "due_date"]:
if field in data:
setattr(self, field, data[field])
if not self.created_at:
self.created_at = datetime.now()
def to_dict(self) -> dict:
d = {}
for f in ["id", "name", "course_id", "description", "due_date", "created_at"]:
d[f] = getattr(self, f)
d["due_date"] = self.due_date.strftime("%Y-%m-%dT%H:%M:%S")
return d
class Content(db.Model):
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(128), index=True)
body = sa.Column(sa.Text, index=True)
course_id = sa.Column(sa.ForeignKey(Course.id), index=True)
created_at = sa.Column(sa.DateTime)
def from_dict(self, data) -> None:
for field in ["name", "body", "course_id"]:
if field in data:
setattr(self, field, data[field])
if not self.created_at:
self.created_at = datetime.now()
def to_dict(self) -> dict:
d = {}
for f in ["id", "course_id", "name", "body", "created_at"]:
d[f] = getattr(self, f)
return d

View File

@@ -1,3 +1,4 @@
from functools import wraps
from flask_login import login_required, login_user, logout_user from flask_login import login_required, login_user, logout_user
from app.bp import bp from app.bp import bp
from flask import jsonify, request from flask import jsonify, request
@@ -5,7 +6,7 @@ from app.errors import error_response
from flask_login import current_user from flask_login import current_user
from app import login, db from app import login, db
from app.models import User from app.models import Content, Course, User, Assignment
@login.user_loader @login.user_loader
@@ -13,6 +14,33 @@ def load_user(user_id):
return User.query.get(user_id) return User.query.get(user_id)
def check_data(data, required_fields):
for f in required_fields:
if f not in data:
return f
return None
def instructor_required(func):
@wraps(func)
def dec(*args, **kwargs):
if not current_user.role in ["instructor", "admin"]:
return error_response(400, "User is not instructor!")
return func(*args, **kwargs)
return dec
def admin_required(func):
@wraps(func)
def dec(*args, **kwargs):
if current_user.role != "admin":
return error_response(400, "User is not an admin!")
return func(*args, **kwargs)
return dec
@bp.route("/login", methods=["POST"]) @bp.route("/login", methods=["POST"])
def login_route(): def login_route():
data = request.get_json() data = request.get_json()
@@ -46,10 +74,8 @@ def logout_route():
@bp.route("/register", methods=["POST"]) @bp.route("/register", methods=["POST"])
def register(): def register():
data = request.get_json() data = request.get_json()
required_fields = ["role", "username", "email", "password", "password2"] required_fields = ["role", "username", "email", "password", "password2"]
for f in required_fields: if f := check_data(data, required_fields):
if f not in data:
return error_response(400, f"Must supply {f}") return error_response(400, f"Must supply {f}")
if User.query.filter_by(username=data["username"]).first(): if User.query.filter_by(username=data["username"]).first():
@@ -65,3 +91,320 @@ def register():
resp = jsonify(u.to_dict()) resp = jsonify(u.to_dict())
return resp return resp
@bp.route("/course", methods=["POST"])
@login_required
@admin_required
def create_course():
data = request.get_json()
required_fields = ["name", "course_code", "description", "instructor"]
if f := check_data(data, required_fields):
return error_response(400, f"Must supply {f}")
u = User.query.get(data["instructor"])
if not u:
return error_response(400, f"User with id {data['instructor']} does not exist")
c = Course.query.filter_by(course_code=data["course_code"]).first()
if c:
return error_response(
400, f"Course with course code {data['course_code']} already exists"
)
if u.role != "instructor":
return error_response(400, "User is not instructor")
c = Course()
c.from_dict(data)
u.enroll(c)
db.session.add(c)
db.session.commit()
return jsonify(c.to_dict())
@bp.route("/course/<string:username>", methods=["POST"])
@login_required
@admin_required
def create_course_by_username(username):
data = request.get_json()
required_fields = ["name", "course_code", "description"]
u = User.query.filter_by(username=username).first()
if not u:
return error_response(400, f"User with username {username} does not exist")
if f := check_data(data, required_fields):
return error_response(400, f"Must supply {f}")
c = Course.query.filter_by(course_code=data["course_code"]).first()
if c:
return error_response(
400, f"Course with course code {data['course_code']} already exists"
)
if u.role != "instructor":
return error_response(400, "User is not instructor")
data["instructor"] = str(u.id)
c = Course()
c.from_dict(data)
u.enroll(c)
db.session.add(c)
db.session.commit()
return jsonify(c.to_dict())
@bp.route("/course/<int:id>", methods=["DELETE"])
@login_required
@admin_required
def delete_course(id):
c = Course.query.get(id)
if not c:
return error_response(400, f"Course with id {id} does not exist")
db.session.delete(c)
db.session.commit()
return jsonify(c.to_dict())
@bp.route("/user/<int:id>/courses", methods=["GET"])
@login_required
def get_courses(id):
u = User.query.get(id)
d = {"courses": []}
courses = Course.query.all() if u.role == "admin" else u.enrolled_courses.all()
for c in courses:
d["courses"].append(c.to_dict())
resp = jsonify(d)
return resp
@bp.route("/course/<int:id>/students", methods=["GET"])
@login_required
def get_students_in_course(id):
c = Course.query.get(id)
if not c:
return error_response(400, f"course with id {id} not found")
students = c.students.filter_by(role="student")
resp = {"students": []}
for s in students:
resp["students"].append(s.to_dict())
return jsonify(resp)
@bp.route("/course/<int:id>/content", methods=["GET"])
@login_required
def get_content_in_course(id):
c = Course.query.get(id)
if not c:
return error_response(400, f"course with id {id} not found")
content = c.content.all()
resp = {"content": []}
for c in content:
resp["content"].append(c.to_dict())
return jsonify(resp)
@bp.route("/course/<int:id>/assignments", methods=["GET"])
@login_required
def get_assignments_in_course(id):
c = Course.query.get(id)
if not c:
return error_response(400, f"course with id {id} not found")
assignments = c.assignments.all()
resp = {"assignments": []}
for a in assignments:
resp["assignments"].append(a.to_dict())
return jsonify(resp)
@bp.route("/course/<int:id>", methods=["GET"])
@login_required
def get_course(id):
c = Course.query.get(id)
if not c:
return error_response(400, f"course with id {id} not found")
resp = jsonify(c.to_dict())
return resp
@bp.route("/user/<int:uid>/enroll/<int:cid>", methods=["POST", "DELETE"])
@login_required
@instructor_required
def enroll_student(uid, cid):
u = User.query.get(uid)
if not u:
return error_response(400, f"User with id {uid} does not exist")
c = Course.query.get(cid)
if not c:
return error_response(400, f"Course with id {cid} does not exist")
if request.method == "POST":
if not u.enroll(c):
return error_response(
400, f"User {uid} is already enrolled in course {cid}"
)
elif request.method == "DELETE":
if not u.unenroll(c):
return error_response(400, f"User {uid} is not enrolled in course {cid}")
resp = {"user": u.to_dict(), "course": c.to_dict()}
return jsonify(resp)
@bp.route("/user/<string:username>/enroll/<int:cid>", methods=["POST"])
@login_required
@instructor_required
def enroll_student_by_username(username, cid):
u = User.query.filter_by(username=username).first()
if not u:
return error_response(400, f"User with username {username} does not exist")
c = Course.query.get(cid)
if not c:
return error_response(400, f"Course with id {cid} does not exist")
if request.method == "POST":
if not u.enroll(c):
return error_response(
400, f"User {u.id} is already enrolled in course {cid}"
)
elif request.method == "DELETE":
if not u.unenroll(c):
return error_response(400, f"User {u.id} is not enrolled in course {cid}")
resp = {"user": u.to_dict(), "course": c.to_dict()}
return jsonify(resp)
@bp.route("/assignment", methods=["POST"])
@login_required
@instructor_required
def create_assignment():
data = request.get_json()
required_fields = ["name", "description", "course_id", "due_date"]
if f := check_data(data, required_fields):
return error_response(400, f"Must supply {f}")
c = Course.query.get(data["course_id"])
if not c:
return error_response(400, f"Course with id {data['course_id']} does not exist")
a = Assignment()
a.from_dict(data)
db.session.add(a)
db.session.commit()
return jsonify(a.to_dict())
@bp.route("/assignment/<int:id>", methods=["GET"])
@login_required
def get_assignment(id):
a = Assignment.query.get(id)
if not a:
return error_response(400, f"Assignment with id {id} does not exist")
return jsonify(a.to_dict())
@bp.route("/assignment/<int:id>", methods=["DELETE"])
@login_required
@instructor_required
def delete_assignment(id):
a = Assignment.query.get(id)
if not a:
return error_response(400, f"Assignment with id {id} does not exist")
db.session.delete(a)
db.session.commit()
return jsonify(a.to_dict())
@bp.route("/assignment/<int:id>", methods=["PUT"])
@login_required
@instructor_required
def update_assignment(id):
a = Assignment.query.get(id)
if not a:
return error_response(400, f"Assignment with id {id} does not exist")
data = request.get_json()
expected_fields = ["name", "description", "due_date"]
for d in data:
if d not in expected_fields:
return error_response(400, f"Field {d} was not expected")
a.from_dict(data)
db.session.commit()
return jsonify(a.to_dict())
@bp.route("/content", methods=["POST"])
@login_required
@instructor_required
def create_content():
data = request.get_json()
required_fields = ["name", "body", "course_id"]
if f := check_data(data, required_fields):
return error_response(400, f"Must supply {f}")
c = Content()
c.from_dict(data)
db.session.add(c)
db.session.commit()
return jsonify(c.to_dict())
@bp.route("/content/<int:id>", methods=["GET"])
@login_required
def get_content(id):
c = Content.query.get(id)
if not c:
return error_response(400, f"Content with id {id} does not exist")
return jsonify(c.to_dict())
@bp.route("/content/<int:id>", methods=["DELETE"])
@login_required
@instructor_required
def delete_content(id):
c = Content.query.get(id)
if not c:
return error_response(400, f"Content with id {id} does not exist")
db.session.delete(c)
db.session.commit()
return jsonify(c.to_dict())
@bp.route("/content/<int:id>", methods=["PUT"])
@login_required
@instructor_required
def update_content(id):
c = Content.query.get(id)
if not c:
return error_response(400, f"Content with id {id} does not exist")
data = request.get_json()
expected_fields = ["name", "body"]
for d in data:
if d not in expected_fields:
return error_response(400, f"Field {d} was not expected")
c.from_dict(data)
db.session.commit()
return jsonify(c.to_dict())

View File

@@ -1,14 +1,19 @@
import dotenv import dotenv
dotenv.load_dotenv() dotenv.load_dotenv()
from app import create_app, db from app import create_app, db
from app.models import User from app.models import Assignment, Course, User
from flask_cors import CORS from flask_cors import CORS
app = create_app() app = create_app()
CORS(app, supports_credentials=True, resources={r"/.*": {"origins": r".*localhost.*"}}) CORS(
app,
supports_credentials=True,
resources={r"/.*": {"origins": [r".*localhost.*", r".*jagrajaulakh.com.*"]}},
)
@app.shell_context_processor @app.shell_context_processor
def make_shell_context(): def make_shell_context():
return {"db": db, "User": User} return {"db": db, "User": User, "Course": Course, "Assignment": Assignment}

View File

@@ -0,0 +1,34 @@
"""Create enrollment table
Revision ID: 093a66f0b581
Revises: 471b4225837e
Create Date: 2023-04-06 16:14:21.262823
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '093a66f0b581'
down_revision = '471b4225837e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('enrollment',
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('course_id', sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
sa.PrimaryKeyConstraint('user_id', 'course_id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('enrollment')
# ### end Alembic commands ###

View File

@@ -0,0 +1,32 @@
"""Added due date to assignment model
Revision ID: 1e88e783d238
Revises: cab0d39ef662
Create Date: 2023-04-13 18:33:37.785568
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '1e88e783d238'
down_revision = 'cab0d39ef662'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('assignment', schema=None) as batch_op:
batch_op.add_column(sa.Column('due_date', sa.DateTime(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('assignment', schema=None) as batch_op:
batch_op.drop_column('due_date')
# ### end Alembic commands ###

View File

@@ -0,0 +1,46 @@
"""Create course model
Revision ID: 471b4225837e
Revises: 8e48199f1417
Create Date: 2023-04-06 14:53:43.061616
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '471b4225837e'
down_revision = '8e48199f1417'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('course',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('instructor', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['instructor'], ['user.id'], ),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('course', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_course_description'), ['description'], unique=False)
batch_op.create_index(batch_op.f('ix_course_instructor'), ['instructor'], unique=False)
batch_op.create_index(batch_op.f('ix_course_name'), ['name'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('course', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_course_name'))
batch_op.drop_index(batch_op.f('ix_course_instructor'))
batch_op.drop_index(batch_op.f('ix_course_description'))
op.drop_table('course')
# ### end Alembic commands ###

View File

@@ -1,4 +1,4 @@
"""empty message """Create user model
Revision ID: 7736bc740f9b Revision ID: 7736bc740f9b
Revises: Revises:

View File

@@ -0,0 +1,34 @@
"""Add course code
Revision ID: 862905f5e34a
Revises: 093a66f0b581
Create Date: 2023-04-06 21:55:54.838647
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '862905f5e34a'
down_revision = '093a66f0b581'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('course', schema=None) as batch_op:
batch_op.add_column(sa.Column('course_code', sa.String(length=32), nullable=True))
batch_op.create_index(batch_op.f('ix_course_course_code'), ['course_code'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('course', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_course_course_code'))
batch_op.drop_column('course_code')
# ### end Alembic commands ###

View File

@@ -1,4 +1,4 @@
"""empty message """Add user role, remove about_me
Revision ID: 8e48199f1417 Revision ID: 8e48199f1417
Revises: 7736bc740f9b Revises: 7736bc740f9b

View File

@@ -0,0 +1,46 @@
"""Create content model
Revision ID: c62aec1c6b91
Revises: 1e88e783d238
Create Date: 2023-04-14 16:46:43.513842
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c62aec1c6b91'
down_revision = '1e88e783d238'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('content',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('body', sa.Text(), nullable=True),
sa.Column('course_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('content', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_content_body'), ['body'], unique=False)
batch_op.create_index(batch_op.f('ix_content_course_id'), ['course_id'], unique=False)
batch_op.create_index(batch_op.f('ix_content_name'), ['name'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('content', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_content_name'))
batch_op.drop_index(batch_op.f('ix_content_course_id'))
batch_op.drop_index(batch_op.f('ix_content_body'))
op.drop_table('content')
# ### end Alembic commands ###

View File

@@ -0,0 +1,46 @@
"""Create assignment model
Revision ID: cab0d39ef662
Revises: 862905f5e34a
Create Date: 2023-04-13 18:27:15.748107
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cab0d39ef662'
down_revision = '862905f5e34a'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('assignment',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('course_id', sa.Integer(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
sa.PrimaryKeyConstraint('id')
)
with op.batch_alter_table('assignment', schema=None) as batch_op:
batch_op.create_index(batch_op.f('ix_assignment_course_id'), ['course_id'], unique=False)
batch_op.create_index(batch_op.f('ix_assignment_description'), ['description'], unique=False)
batch_op.create_index(batch_op.f('ix_assignment_name'), ['name'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('assignment', schema=None) as batch_op:
batch_op.drop_index(batch_op.f('ix_assignment_name'))
batch_op.drop_index(batch_op.f('ix_assignment_description'))
batch_op.drop_index(batch_op.f('ix_assignment_course_id'))
op.drop_table('assignment')
# ### end Alembic commands ###

View File

@@ -12,6 +12,7 @@ itsdangerous==2.1.2
Jinja2==3.1.2 Jinja2==3.1.2
Mako==1.2.4 Mako==1.2.4
MarkupSafe==2.1.2 MarkupSafe==2.1.2
mysqlclient==2.1.1
python-dotenv==1.0.0 python-dotenv==1.0.0
six==1.16.0 six==1.16.0
SQLAlchemy==2.0.6 SQLAlchemy==2.0.6

View File

@@ -3,12 +3,31 @@ services:
frontend: frontend:
image: comp2707-frontend image: comp2707-frontend
build: frontend/ build: frontend/
depends_on:
- backend
container_name: comp2707-frontend container_name: comp2707-frontend
ports: ports:
- 8080:8080 - 8080:8080
backend: backend:
image: comp2707-backend image: comp2707-backend
build: backend/ build: backend/
depends_on:
- db
container_name: comp2707-backend container_name: comp2707-backend
environment:
- DATABASE_URL=mysql://root:mama@db/2707
ports: ports:
- 5000:5000 - 5001:5000
db:
image: mariadb:latest
environment:
- MARIADB_ROOT_PASSWORD=mama
- MARIADB_DATABASE=2707
volumes:
- db-volume:/var/lib/mysql
ports:
- 3406:3306
volumes:
db-volume:
driver: local

2
frontend/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
.env.local
node_modules/

1
frontend/.env Normal file
View File

@@ -0,0 +1 @@
REACT_APP_BACKEND_URL=http://localhost:5000

1
frontend/.env.production Normal file
View File

@@ -0,0 +1 @@
REACT_APP_BACKEND_URL=http://be.2707.jagrajaulakh.com:5001

View File

@@ -8,6 +8,7 @@ RUN mkdir /code && cp -a /tmp/node_modules /code/
# Copy all the source code # Copy all the source code
WORKDIR /code WORKDIR /code
COPY ./ /code COPY ./ /code
COPY .env.production .env
# Build the project # Build the project
RUN ["npm", "run", "build"] RUN ["npm", "run", "build"]

View File

@@ -12,6 +12,7 @@
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"dotenv": "^16.0.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.2", "react-bootstrap": "^2.7.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@@ -6868,11 +6869,11 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "10.0.0", "version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==",
"engines": { "engines": {
"node": ">=10" "node": ">=12"
} }
}, },
"node_modules/dotenv-expand": { "node_modules/dotenv-expand": {
@@ -14577,6 +14578,14 @@
} }
} }
}, },
"node_modules/react-scripts/node_modules/dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
"engines": {
"node": ">=10"
}
},
"node_modules/react-transition-group": { "node_modules/react-transition-group": {
"version": "4.4.5", "version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
@@ -22349,9 +22358,9 @@
} }
}, },
"dotenv": { "dotenv": {
"version": "10.0.0", "version": "16.0.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ=="
}, },
"dotenv-expand": { "dotenv-expand": {
"version": "5.1.0", "version": "5.1.0",
@@ -27743,6 +27752,13 @@
"webpack-dev-server": "^4.6.0", "webpack-dev-server": "^4.6.0",
"webpack-manifest-plugin": "^4.0.2", "webpack-manifest-plugin": "^4.0.2",
"workbox-webpack-plugin": "^6.4.1" "workbox-webpack-plugin": "^6.4.1"
},
"dependencies": {
"dotenv": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q=="
}
} }
}, },
"react-transition-group": { "react-transition-group": {

View File

@@ -7,6 +7,7 @@
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"bootstrap": "^5.2.3", "bootstrap": "^5.2.3",
"dotenv": "^16.0.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.2", "react-bootstrap": "^2.7.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

View File

@@ -22,7 +22,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>LearningTree</title>
</head> </head>
<body> <body>

View File

@@ -1,6 +1,9 @@
const express = require("express"); const express = require("express");
const path = require("path"); const path = require("path");
const app = express(); const app = express();
const dotenv = require('dotenv');
dotenv.config()
console.log(process.env);
app.use("/*", (req, res, next) => { app.use("/*", (req, res, next) => {
now = new Date(); now = new Date();

View File

@@ -4,31 +4,117 @@ import HomePage from "./pages/HomePage";
import LoginPage from "./pages/LoginPage"; import LoginPage from "./pages/LoginPage";
import LogoutPage from "./pages/LogoutPage"; import LogoutPage from "./pages/LogoutPage";
import RegisterPage from "./pages/RegisterPage"; import RegisterPage from "./pages/RegisterPage";
import CoursePage from "./pages/CoursePage";
import AssignmentPage from "./pages/AssignmentPage";
import ContentPage from "./pages/ContentPage";
import ManagePage from "./pages/ManagePage";
import ManageStudentsPage from "./pages/ManageStudentsPage";
import AuthenticatedRoute from "./components/AuthenticatedRoute"; import AuthenticatedRoute from "./components/AuthenticatedRoute";
import ManageAssignmentsPage from "./pages/ManageAssignmentsPage";
import ManageContentPage from "./pages/ManageContentPage";
import AssignmentEditPage from "./pages/AssignmentEditPage";
const AuthRoute = ({ isAuthenticated = true, path, children }) => {
return (
<Route path={path}>
<AuthenticatedRoute isAuthenticated={isAuthenticated}>
{children}
</AuthenticatedRoute>
</Route>
);
};
function App() { function App() {
return ( return (
<div className="App"> <div className="App">
<Route path="/login"> <AuthRoute path="/login" isAuthenticated={false}>
<AuthenticatedRoute isAuthenticated={false}>
<LoginPage /> <LoginPage />
</AuthenticatedRoute> </AuthRoute>
</Route> <AuthRoute path="/logout" isAuthenticated={false}>
<Route path="/logout">
<AuthenticatedRoute isAuthenticated={false}>
<LogoutPage /> <LogoutPage />
</AuthenticatedRoute> </AuthRoute>
</Route> <AuthRoute path="/register" isAuthenticated={false}>
<Route path="/register">
<AuthenticatedRoute isAuthenticated={false}>
<RegisterPage /> <RegisterPage />
</AuthenticatedRoute> </AuthRoute>
</Route>
<Route path="/"> <AuthRoute path="/">
<AuthenticatedRoute isAuthenticated={true}>
<HomePage /> <HomePage />
</AuthRoute>
<Route path="/course/:id">
{(params) => {
return (
<AuthenticatedRoute>
<CoursePage id={params.id} />
</AuthenticatedRoute> </AuthenticatedRoute>
);
}}
</Route> </Route>
<Route path="/content/:id">
{(params) => {
return (
<AuthenticatedRoute>
<ContentPage id={params.id} />
</AuthenticatedRoute>
);
}}
</Route>
<Route path="/assignment/:id/edit">
{(params) => {
return (
<AuthenticatedRoute>
<AssignmentEditPage id={params.id} />
</AuthenticatedRoute>
);
}}
</Route>
<Route path="/assignment/:id">
{(params) => {
return (
<AuthenticatedRoute>
<AssignmentPage id={params.id} />
</AuthenticatedRoute>
);
}}
</Route>
<AuthRoute path="/manage">
<ManagePage />
</AuthRoute>
<Route path="/manage/:cid/students">
{(params) => {
return (
<AuthenticatedRoute>
<ManageStudentsPage cid={params.cid} />
</AuthenticatedRoute>
);
}}
</Route>
<Route path="/manage/:id/content">
{(params) => {
return (
<AuthenticatedRoute>
<ManageContentPage cid={params.id} />
</AuthenticatedRoute>
);
}}
</Route>
<Route path="/manage/:id/assignments">
{(params) => {
return (
<AuthenticatedRoute>
<ManageAssignmentsPage cid={params.id} />
</AuthenticatedRoute>
);
}}
</Route>
</div> </div>
); );
} }

View File

@@ -0,0 +1,43 @@
import { useContext, useEffect, useState } from "react";
import { Card, Container } from "react-bootstrap";
import { Link } from "wouter";
import UserContext from "../contexts/UserContext";
import { makeRequest } from "../utils.ts";
const AssignmentsWidget = ({ className = "", cid }) => {
const [assignmentData, setAssignmentData] = useState([]);
useEffect(() => {
makeRequest({ endpoint: `course/${cid}/assignments` })
.then((resp) => resp.json())
.then((data) => {
setAssignmentData(data.assignments);
});
}, [setAssignmentData]);
return (
<Container className={`${className} py-3 grid`}>
<div className="row justify-content-center">
{assignmentData.map((assignment, i) => {
return (
<Link
is="a"
key={i}
href={`/assignment/${assignment.id}`}
className="col col-lg-2"
>
<Card role="button" className="m-2" style={{ width: "300px" }}>
<Card.Body>
<Card.Title className="text-center">{assignment.name}</Card.Title>
<Card.Text>Due: {assignment.due_date}</Card.Text>
</Card.Body>
</Card>
</Link>
);
})}
</div>
</Container>
);
};
export default AssignmentsWidget;

View File

@@ -2,7 +2,7 @@ import { useContext, useEffect } from "react";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
import UserContext from "../contexts/UserContext"; import UserContext from "../contexts/UserContext";
const AuthenticatedRoute = ({ children, isAuthenticated }) => { const AuthenticatedRoute = ({ children, isAuthenticated=true }) => {
const { currentUser } = useContext(UserContext); const { currentUser } = useContext(UserContext);
const [location, setLocation] = useLocation(); const [location, setLocation] = useLocation();
@@ -12,7 +12,7 @@ const AuthenticatedRoute = ({ children, isAuthenticated }) => {
} else if (!isAuthenticated && currentUser?.id) { } else if (!isAuthenticated && currentUser?.id) {
setLocation("/"); setLocation("/");
} }
}, [currentUser]); }, [currentUser, setLocation, isAuthenticated]);
return children; return children;
}; };

View File

@@ -0,0 +1,42 @@
import { useContext, useEffect, useState } from "react";
import { Card, Container } from "react-bootstrap";
import { Link } from "wouter";
import UserContext from "../contexts/UserContext";
import { makeRequest } from "../utils.ts";
const ContentWidget = ({ className = "", cid }) => {
const [contentData, setContentData] = useState([]);
useEffect(() => {
makeRequest({ endpoint: `course/${cid}/content` })
.then((resp) => resp.json())
.then((data) => {
setContentData(data.content);
});
}, [setContentData]);
return (
<Container className={`${className} py-3 grid`}>
<div className="row justify-content-center">
{contentData.map((content, i) => {
return (
<Link
is="a"
key={i}
href={`/content/${content.id}`}
className="col col-lg-2"
>
<Card role="button" className="m-2" style={{ width: "300px" }}>
<Card.Body>
<Card.Title className="text-center">{content.name}</Card.Title>
</Card.Body>
</Card>
</Link>
);
})}
</div>
</Container>
);
};
export default ContentWidget;

View File

@@ -0,0 +1,50 @@
import { useContext, useEffect, useState } from "react";
import { Card, Container } from "react-bootstrap";
import { Link } from "wouter";
import UserContext from "../contexts/UserContext";
import { makeRequest } from "../utils.ts";
const CoursesWidget = ({ className = "" }) => {
const [courseData, setCourseData] = useState([]);
const { currentUser } = useContext(UserContext);
useEffect(() => {
if (!currentUser.id) {
return;
}
makeRequest({ endpoint: `user/${currentUser.id}/courses` })
.then((resp) => resp.json())
.then((data) => {
setCourseData(data.courses);
});
}, [setCourseData, currentUser]);
return (
<Container className={`${className} py-3 grid`}>
<div className="row justify-content-center">
{courseData.map((course, i) => {
return (
<Link
is="a"
key={i}
href={`/course/${course.id}`}
className="col col-lg-2"
>
<Card role="button" className="m-2" style={{ width: "300px" }}>
<h2 className="text-center py-5 border">
{course.course_code}
</h2>
<Card.Body>
<Card.Title>{course.name}</Card.Title>
<Card.Text>{course.instructor}</Card.Text>
</Card.Body>
</Card>
</Link>
);
})}
</div>
</Container>
);
};
export default CoursesWidget;

View File

@@ -6,27 +6,42 @@ import UserContext from "../contexts/UserContext";
const MyNavbar = () => { const MyNavbar = () => {
const { currentUser } = useContext(UserContext); const { currentUser } = useContext(UserContext);
const instructorLinks = [
{
label: "Manage Courses",
link: "/manage",
},
];
const MyLink = ({ children, ...rest }) => { const MyLink = ({ children, ...rest }) => {
return <Nav.Link as={Link} {...rest}>{children}</Nav.Link>; return (
<Nav.Link as={Link} {...rest}>
{children}
</Nav.Link>
);
}; };
return ( return (
<React.Fragment>
<Navbar variant="dark" bg="dark" expand="lg"> <Navbar variant="dark" bg="dark" expand="lg">
<Container> <Container>
<Navbar.Brand href="/">LearningTree</Navbar.Brand> <Navbar.Brand as={Link} href="/">
LearningTree
</Navbar.Brand>
<Navbar.Toggle aria-controls="navbar-nav" /> <Navbar.Toggle aria-controls="navbar-nav" />
<Navbar.Collapse id="navbar-nav"> <Navbar.Collapse id="navbar-nav">
<Nav className="ms-auto"> <Nav className="ms-auto">
<MyLink href="/">Home</MyLink> <MyLink href="/">Home</MyLink>
{(currentUser?.id && ( {(currentUser?.role === "instructor" || currentUser?.role === "admin") &&
<MyLink href="/logout">Logout</MyLink> instructorLinks.map((item, k) => {
)) || <MyLink href="/login">Login</MyLink>} return <MyLink key={k} href={item.link}>{item.label}</MyLink>;
})}
{(currentUser?.id && <MyLink href="/logout">Logout</MyLink>) || (
<MyLink href="/login">Login</MyLink>
)}
</Nav> </Nav>
</Navbar.Collapse> </Navbar.Collapse>
</Container> </Container>
</Navbar> </Navbar>
</React.Fragment>
); );
}; };

View File

@@ -0,0 +1,90 @@
import { useEffect, useState } from "react";
import { Container, Table, Button, Form } from "react-bootstrap";
import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts";
import { useLocation } from "wouter";
const AssignmentEditPage = ({ id }) => {
const [assignmentData, setAssignmentData] = useState({});
const [location, setLocation] = useLocation();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [duedate, setDuedate] = useState("");
const submitAssignmentForm = (name, description, duedate) => {
makeRequest({
endpoint: `assignment/${id}`,
method: "PUT",
body: {
name,
description,
due_date: duedate,
},
}).then((resp) => {
setLocation(`/manage/${assignmentData.course_id}/assignments`);
});
};
useEffect(() => {
makeRequest({ endpoint: `assignment/${id}` })
.then((resp) => resp.json())
.then((data) => {
setAssignmentData(data);
setName(data.name);
setDescription(data.description);
setDuedate(data.due_date);
});
}, []);
return (
<div>
<MyNavbar />
<Container className="p-5 border">
{assignmentData.name && (
<Form
className="my-4 p-2 border"
onSubmit={(e) => {
e.preventDefault();
submitAssignmentForm(name, description, duedate);
}}
>
<Form.Group controlId="assignmentName">
<Form.Label>Name</Form.Label>
<Form.Control
value={name}
type="text"
placeholder="Enter assignment name"
onChange={(e) => setName(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="assignmentDescription">
<Form.Label>Description</Form.Label>
<Form.Control
as="textarea"
value={description}
rows={4}
placeholder="Enter assignment description"
onChange={(e) => setDescription(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="assignmentDuedate">
<Form.Label>Due Date</Form.Label>
<Form.Control
value={duedate}
type="datetime-local"
placeholder="Due date"
onChange={(e) => setDuedate(e.target.value)}
/>
</Form.Group>
<Button className="my-2" type="submit" variant="success">
Submit
</Button>
</Form>
)}
</Container>
</div>
);
};
export default AssignmentEditPage;

View File

@@ -0,0 +1,50 @@
import { useEffect, useState } from "react";
import { Container } from "react-bootstrap";
import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts";
const AssignmentPage = ({ id }) => {
const [assignmentData, setAssignmentData] = useState({});
useEffect(() => {
makeRequest({ endpoint: `assignment/${id}` })
.then((resp) => resp.json())
.then((data) => {
setAssignmentData(data);
});
}, []);
const Title = ({ children, className, ...rest }) => {
return (
<p className={`${className} fs-6`} {...rest}>
{children}
</p>
);
};
const Value = ({ children, className, ...rest }) => {
return (
<p className={`${className} fs-4`} {...rest}>
{children}
</p>
);
};
return (
<div>
<MyNavbar />
<Container className="p-5 border">
<Title>Name</Title>
<Value>{assignmentData.name}</Value>
<hr />
<Title>Description</Title>
<Value>{assignmentData.description}</Value>
<hr />
<Title>Due Date</Title>
<Value>{assignmentData.due_date}</Value>
</Container>
</div>
);
};
export default AssignmentPage;

View File

@@ -0,0 +1,65 @@
import { useEffect, useState } from "react";
import { Container } from "react-bootstrap";
import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts";
const ContentPage = ({ id }) => {
const [contentData, setContentData] = useState({});
const [courseData, setCourseData] = useState({});
useEffect(() => {
const wrap = async () => {
let resp = await makeRequest({ endpoint: `content/${id}` });
let data = await resp.json();
setContentData(data);
resp = await makeRequest({ endpoint: `course/${data.course_id}` });
data = await resp.json();
setCourseData(data);
};
wrap();
}, []);
const Title = ({ children, className, ...rest }) => {
return (
<p className={`${className} fs-6`} {...rest}>
{children}
</p>
);
};
const Value = ({ children, className, ...rest }) => {
return (
<div className={`${className} fs-4`} {...rest}>
{children}
</div>
);
};
return (
<div>
<MyNavbar />
<Container className="p-5 border">
<h1>{courseData.name}</h1>
<hr />
<Container className="p-5 border">
<Title>Name</Title>
<Value>{contentData.name}</Value>
<hr />
<Title>Created</Title>
<Value>{contentData.created_at}</Value>
<hr />
<Title>Body</Title>
<Value>
{contentData.body?.split("\n").map((line, k) => {
return <div key={k}>{line}</div>;
})}
</Value>
</Container>
</Container>
</div>
);
};
export default ContentPage;

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
import { Container } from "react-bootstrap";
import AssignmentsWidget from "../components/AssignmentsWidget";
import ContentWidget from "../components/ContentWidget";
import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts";
const CoursePage = ({ id }) => {
const [courseData, setCourseData] = useState({});
useEffect(() => {
makeRequest({ endpoint: `course/${id}` })
.then((resp) => resp.json())
.then((data) => {
setCourseData(data);
});
}, []);
return (
<div>
<MyNavbar />
<Container className="p-5 border">
<h1>{courseData.name}</h1>
<h4 className="mb-4">{courseData.instructor}</h4>
<hr />
<div className="border">
<h3>Course Content</h3>
<ContentWidget cid={id} />
<hr />
<h3>Assignments</h3>
<AssignmentsWidget cid={id} />
</div>
</Container>
</div>
);
};
export default CoursePage;

View File

@@ -1,5 +1,6 @@
import { useContext, useEffect } from "react"; import { useContext } from "react";
import { Container } from "react-bootstrap"; import { Container } from "react-bootstrap";
import CoursesWidget from "../components/CoursesWidget";
import MyNavbar from "../components/MyNavbar"; import MyNavbar from "../components/MyNavbar";
import UserContext from "../contexts/UserContext"; import UserContext from "../contexts/UserContext";
@@ -13,6 +14,13 @@ const HomePage = () => {
<div> <div>
<h1>Welcome back {currentUser?.username}!</h1> <h1>Welcome back {currentUser?.username}!</h1>
</div> </div>
<br />
<br />
<br />
<div>
<h2>Courses</h2>
<CoursesWidget className="ms-0 border" />
</div>
</Container> </Container>
</div> </div>
); );

View File

@@ -1,9 +1,9 @@
import React, { useContext, useEffect, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import { Button, Col, Container, Form, Row, Alert } from "react-bootstrap"; import { Button, Col, Container, Form, Row, Alert } from "react-bootstrap";
import { Link } from "wouter"; import { Link, useLocation } from "wouter";
import MyNavbar from "../components/MyNavbar"; import MyNavbar from "../components/MyNavbar";
import UserContext from "../contexts/UserContext"; import UserContext from "../contexts/UserContext";
import { makeRequest } from "../utils.ts"; import { sendLogoutRequest, sendLoginRequest } from "../utils.ts";
const LoginPage = () => { const LoginPage = () => {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
@@ -11,32 +11,32 @@ const LoginPage = () => {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const { currentUser, setCurrentUser } = useContext(UserContext); const { currentUser, setCurrentUser } = useContext(UserContext);
const [location, setLocation] = useLocation();
useEffect(() => { useEffect(() => {
if (currentUser?.id) { if (currentUser?.id) {
gotoHome(); setLocation("/");
} }
}, [currentUser]); }, [currentUser, setLocation]);
const gotoHome = () => { const formCb = async (e) => {
window.location.href = "/";
};
const sendLoginRequest = async (e) => {
e?.preventDefault(); e?.preventDefault();
await makeRequest({ const res = await sendLoginRequest(username, password);
url: "http://localhost:5000/login",
method: "POST", if (res?.isError) {
body: { username, password }, if (res.message.includes("already")) {
}) setError({
.then((resp) => resp.json()) ...res,
.then((data) => { message: "An error occured. Try logging in again.",
if (data.error) { });
setError(data); await sendLogoutRequest();
return; return;
} }
setCurrentUser(data); setError(res);
}); return;
}
setCurrentUser(res);
setLocation("/");
}; };
return ( return (
@@ -52,7 +52,7 @@ const LoginPage = () => {
)} )}
<h2>Login</h2> <h2>Login</h2>
<br /> <br />
<Form onSubmit={sendLoginRequest}> <Form onSubmit={formCb}>
<Form.Group as={Row} className="mb-3" controlId="username"> <Form.Group as={Row} className="mb-3" controlId="username">
<Form.Label column sm={2} className="me-2"> <Form.Label column sm={2} className="me-2">
Username Username

View File

@@ -3,23 +3,20 @@ import { useContext, useEffect } from "react";
import { Container } from "react-bootstrap"; import { Container } from "react-bootstrap";
import MyNavbar from "../components/MyNavbar"; import MyNavbar from "../components/MyNavbar";
import UserContext from "../contexts/UserContext"; import UserContext from "../contexts/UserContext";
import { makeRequest } from "../utils.ts"; import { sendLogoutRequest } from "../utils.ts";
const LogoutPage = () => { const LogoutPage = () => {
const { setCurrentUser } = useContext(UserContext); const { setCurrentUser } = useContext(UserContext);
useEffect(() => { useEffect(() => {
const cleanup = async () => { (async () => {
await makeRequest({
url: "http://localhost:5000/logout",
method: "POST",
});
await setCurrentUser({});
localStorage.removeItem("currentUser"); localStorage.removeItem("currentUser");
try {
await sendLogoutRequest();
} catch (e) {}
await setCurrentUser({});
window.location.href = "/login"; window.location.href = "/login";
}; })();
cleanup(); }, [setCurrentUser]);
}, []);
return ( return (
<React.Fragment> <React.Fragment>

View File

@@ -0,0 +1,172 @@
import { useEffect, useState } from "react";
import { Container, Table, Button, Form } from "react-bootstrap";
import { useLocation } from "wouter";
import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts";
const ManageAssignmentsPage = ({ cid }) => {
const [courseData, setCourseData] = useState({});
const [assignmentData, setAssignmentData] = useState([]);
const [showAddAssignmentForm, setShowAddAssignmentForm] = useState(false);
const [location, setLocation] = useLocation();
const AddAssignmentForm = () => {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [duedate, setDuedate] = useState("");
return (
<div>
<Form
className="my-4 p-2 border"
onSubmit={() => {
submitAssignmentForm(name, description, duedate);
}}
>
<Form.Group controlId="assignmentName">
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter assignment name"
onChange={(e) => setName(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="assignmentDescription">
<Form.Label>Description</Form.Label>
<Form.Control
as="textarea"
rows={4}
placeholder="Enter assignment description"
onChange={(e) => setDescription(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="assignmentDuedate">
<Form.Label>Due Date</Form.Label>
<Form.Control
type="datetime-local"
step={1}
placeholder="Due date"
onChange={(e) => setDuedate(e.target.value)}
/>
</Form.Group>
<Button className="my-2" type="submit" variant="success">
Submit
</Button>
</Form>
</div>
);
};
const submitAssignmentForm = (name, description, duedate) => {
makeRequest({
endpoint: "assignment",
method: "POST",
body: {
name,
description,
course_id: cid,
due_date: duedate,
},
});
};
const sendDeleteAssignmentRequest = (id) => {
makeRequest({
endpoint: `assignment/${id}`,
method: "DELETE",
}).then((resp) => {
window.location.reload();
});
};
useEffect(() => {
makeRequest({
endpoint: `course/${cid}/assignments`,
method: "GET",
})
.then((req) => req.json())
.then(async (data) => {
setAssignmentData(data.assignments);
})
.catch((err) => {
console.error(err);
});
makeRequest({
endpoint: `course/${cid}`,
method: "GET",
})
.then((req) => req.json())
.then(async (data) => {
setCourseData(data);
})
.catch((err) => {
console.error(err);
});
}, []);
return (
<div>
<MyNavbar />
<Container className="p-5 border">
<h1 className="mb-4">{courseData.name}</h1>
<h3 className="mb-4">Manage Assignments</h3>
<Button
className="my-3"
variant="primary"
onClick={() => {
setShowAddAssignmentForm(!showAddAssignmentForm);
}}
>
{(showAddAssignmentForm && "-") || "+"} Add assignment
</Button>
{showAddAssignmentForm && <AddAssignmentForm />}
<Table bordered striped hover>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Due date</th>
<th>Manage</th>
</tr>
</thead>
<tbody>
{assignmentData.map((assignment, k) => {
return (
<tr key={k}>
<td>{assignment.id}</td>
<td>{assignment.name}</td>
<td>{assignment.due_date}</td>
<td>
<div>
<Button
className="mx-1"
variant="primary"
onClick={() =>
setLocation(`/assignment/${assignment.id}/edit`)
}
>
Edit
</Button>
<Button
className="mx-1"
variant="danger"
onClick={() =>
sendDeleteAssignmentRequest(assignment.id)
}
>
Delete
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</Table>
</Container>
</div>
);
};
export default ManageAssignmentsPage;

View File

@@ -0,0 +1,149 @@
import { useEffect, useState } from "react";
import { Container, Table, Button, Form } from "react-bootstrap";
import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts";
const ManageContentPage = ({ cid }) => {
const [courseData, setCourseData] = useState({});
const [contentData, setContentData] = useState([]);
const [showAddContentForm, setShowAddContentForm] = useState(false);
const AddContentForm = () => {
const [name, setName] = useState("");
const [body, setBody] = useState("");
return (
<div>
<Form
className="my-4 p-2 border"
onSubmit={() => {
submitContentForm(name, body);
}}
>
<Form.Group controlId="contentName">
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter content name"
onChange={(e) => setName(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="contentBody">
<Form.Label>Body</Form.Label>
<Form.Control
as="textarea"
rows={4}
placeholder="Enter content body"
onChange={(e) => setBody(e.target.value)}
/>
</Form.Group>
<Button className="my-2" type="submit" variant="success">
Submit
</Button>
</Form>
</div>
);
};
const submitContentForm = (name, body) => {
makeRequest({
endpoint: "content",
method: "POST",
body: {
name,
body,
course_id: cid,
},
});
};
const sendDeleteContentRequest = (id) => {
makeRequest({
endpoint: `content/${id}`,
method: "DELETE",
}).then((resp) => {
window.location.reload();
});
};
useEffect(() => {
makeRequest({
endpoint: `course/${cid}/content`,
method: "GET",
})
.then((req) => req.json())
.then(async (data) => {
setContentData(data.content);
})
.catch((err) => {
console.error(err);
});
makeRequest({
endpoint: `course/${cid}`,
method: "GET",
})
.then((req) => req.json())
.then(async (data) => {
setCourseData(data);
})
.catch((err) => {
console.error(err);
});
}, []);
return (
<div>
<MyNavbar />
<Container className="p-5 border">
<h1 className="mb-4">{courseData.name}</h1>
<h3 className="mb-4">Manage Course Content</h3>
<Button
className="my-3"
variant="primary"
onClick={() => {
setShowAddContentForm(!showAddContentForm);
}}
>
{(showAddContentForm && "-") || "+"} Add course content
</Button>
{showAddContentForm && <AddContentForm />}
<Table bordered striped hover>
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Created</th>
<th>Manage</th>
</tr>
</thead>
<tbody>
{contentData.map((content, k) => {
return (
<tr key={k}>
<td>{content.id}</td>
<td>{content.name}</td>
<td>{content.created_at}</td>
<td>
<div>
<Button
variant="danger"
onClick={() =>
sendDeleteContentRequest(content.id)
}
>
Delete
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</Table>
</Container>
</div>
);
};
export default ManageContentPage;

View File

@@ -0,0 +1,181 @@
import React from "react";
import { useContext, useEffect, useState } from "react";
import { Form, Button, Container, Table } from "react-bootstrap";
import { Link } from "wouter";
import MyNavbar from "../components/MyNavbar";
import UserContext from "../contexts/UserContext";
import { makeRequest } from "../utils.ts";
const ManagePage = () => {
const [courseData, setCourseData] = useState([]);
const { currentUser } = useContext(UserContext);
const [showAddCourseForm, setShowAddCourseForm] = useState(false);
const AddCourseForm = () => {
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [username, setUsername] = useState("");
const [coursecode, setCoursecode] = useState("");
const submitCourseForm = () => {
makeRequest({
endpoint: `course/${username}`,
method: "POST",
body: {
course_code: coursecode,
name,
description,
},
})
.then((resp) => resp.json())
.then((data) => {
window.location.reload();
});
};
return (
<div>
<Form
className="my-4 p-2 border justify-between items-center"
onSubmit={(e) => {
e.preventDefault();
submitCourseForm();
}}
>
<Form.Group controlId="courseCode">
<Form.Label>Course code</Form.Label>
<Form.Control
type="text"
placeholder="Enter course code"
onChange={(e) => setCoursecode(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="courseName">
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter course name"
onChange={(e) => setName(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="courseDescription">
<Form.Label>Description</Form.Label>
<Form.Control
type="text"
placeholder="Enter course description"
onChange={(e) => setDescription(e.target.value)}
/>
</Form.Group>
<Form.Group controlId="courseInstructor">
<Form.Label>Instructor Username</Form.Label>
<Form.Control
type="text"
placeholder="Instructor"
onChange={(e) => setUsername(e.target.value)}
/>
</Form.Group>
<Button className="my-2" type="submit" variant="success">
Submit
</Button>
</Form>
</div>
);
};
const sendDeleteCourseRequest = (cid) => {
makeRequest({
endpoint: `course/${cid}`,
method: "DELETE",
})
.then((resp) => resp.json())
.then((data) => {
window.location.reload();
});
};
useEffect(() => {
makeRequest({
endpoint: `user/${currentUser.id}/courses`,
method: "GET",
})
.then((req) => req.json())
.then((data) => {
setCourseData(data.courses);
})
.catch((err) => {
console.error(err);
});
}, []);
return (
<div>
<MyNavbar />
<Container className="p-5 border">
<h1 className="mb-4">Manage Courses</h1>
{currentUser.role === "admin" && (
<Button
className="my-3"
variant="primary"
onClick={() => {
setShowAddCourseForm(!showAddCourseForm);
}}
>
{(showAddCourseForm && "-") || "+"} Add course
</Button>
)}
{showAddCourseForm && <AddCourseForm />}
<Table bordered striped hover>
<thead>
<tr>
<th>#</th>
<th>Course Code</th>
<th>Name</th>
<th>Instructor</th>
<th>Manage</th>
</tr>
</thead>
<tbody>
{courseData.map((course, k) => {
return (
<tr key={k}>
<td>{course.id}</td>
<td>{course.course_code}</td>
<td>{course.name}</td>
<td>{course.instructor}</td>
<td>
<div>
<Link href={`/manage/${course.id}/students`}>
Students
</Link>
<br />
<Link href={`/manage/${course.id}/content`}>
Course Content
</Link>
<br />
<Link href={`/manage/${course.id}/assignments`}>
Assignments
</Link>
{currentUser?.role === "admin" && (
<React.Fragment>
<br />
<Button
variant="danger"
onClick={() => sendDeleteCourseRequest(course.id)}
>
Delete
</Button>
</React.Fragment>
)}
</div>
</td>
</tr>
);
})}
</tbody>
</Table>
</Container>
</div>
);
};
export default ManagePage;

View File

@@ -0,0 +1,123 @@
import { useEffect, useState } from "react";
import { Container, Table, Button, Form } from "react-bootstrap";
import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts";
const ManageStutentsPage = ({ cid }) => {
const [studentData, setStudentData] = useState([]);
const [showAddStudentForm, setShowAddStudentForm] = useState(false);
const submitStudentForm = async (username) => {
await makeRequest({
endpoint: `user/${username}/enroll/${cid}`,
method: "POST",
});
window.location.reload();
return false;
};
const AddStudentForm = () => {
const [username, setUsername] = useState("");
return (
<div>
<Form
className="my-4 p-2 border"
onSubmit={() => {
submitStudentForm(username);
}}
>
<Form.Group controlId="studentUsername">
<Form.Label>Username</Form.Label>
<Form.Control
type="text"
placeholder="Enter student's username"
onChange={(e) => setUsername(e.target.value)}
/>
<Form.Text>
Make sure that a user with the username exists and is not already
enrolled in this course
</Form.Text>
</Form.Group>
<Button className="my-2" type="submit" variant="success">
Submit
</Button>
</Form>
</div>
);
};
useEffect(() => {
makeRequest({
endpoint: `course/${cid}/students`,
method: "GET",
})
.then((req) => req.json())
.then(async (data) => {
setStudentData(data.students);
})
.catch((err) => {
console.error(err);
});
}, []);
const sendUnenrollRequest = (uid) => {
makeRequest({
endpoint: `user/${uid}/enroll/${cid}`,
method: "DELETE",
}).then((resp) => {
window.location.reload();
});
};
return (
<div>
<MyNavbar />
<Container className="p-5 border">
<h1 className="mb-4">Manage Students</h1>
<Button
className="my-3"
variant="primary"
onClick={() => {
setShowAddStudentForm(!showAddStudentForm);
}}
>
{(showAddStudentForm && "-") || "+"} Add student
</Button>
{showAddStudentForm && <AddStudentForm />}
<Table bordered striped hover>
<thead>
<tr>
<th>#</th>
<th>Username</th>
<th>Email</th>
<th>Manage</th>
</tr>
</thead>
<tbody>
{studentData.map((student, k) => {
return (
<tr key={k}>
<td>{student.id}</td>
<td>{student.username}</td>
<td>{student.email}</td>
<td>
<div>
<Button
variant="danger"
onClick={() => sendUnenrollRequest(student.id)}
>
Unenroll
</Button>
</div>
</td>
</tr>
);
})}
</tbody>
</Table>
</Container>
</div>
);
};
export default ManageStutentsPage;

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Col, Container, Form, Row, Alert } from "react-bootstrap"; import { Button, Col, Container, Form, Row, Alert } from "react-bootstrap";
import { Link } from "wouter"; import { Link, useLocation } from "wouter";
import MyNavbar from "../components/MyNavbar"; import MyNavbar from "../components/MyNavbar";
import { makeRequest } from "../utils.ts"; import { makeRequest } from "../utils.ts";
@@ -12,11 +12,12 @@ const RegisterPage = () => {
const [password2, setPassword2] = useState(""); const [password2, setPassword2] = useState("");
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [location, setLocation] = useLocation();
const sendRegisterRequest = (e) => { const sendRegisterRequest = (e) => {
e?.preventDefault(); e?.preventDefault();
makeRequest({ makeRequest({
url: "http://localhost:5000/register", endpoint: "register",
method: "POST", method: "POST",
body: { body: {
role, role,
@@ -32,7 +33,7 @@ const RegisterPage = () => {
setError(data); setError(data);
return; return;
} }
window.location.href = "/login"; setLocation("/login");
}); });
}; };
@@ -61,7 +62,8 @@ const RegisterPage = () => {
}} }}
> >
<option value="student">Student</option> <option value="student">Student</option>
<option value="teacher">Teacher</option> <option value="instructor">Instructor</option>
<option value="admin">Admin</option>
</Form.Select> </Form.Select>
</Col> </Col>
</Form.Group> </Form.Group>

View File

@@ -1,11 +1,53 @@
const makeRequest = ({ url, method, body }): Promise<Response> => { const { REACT_APP_BACKEND_URL } = process.env;
return fetch(url, {
const makeRequest = ({ endpoint, method, body = null }): Promise<Response> => {
const req: RequestInit = {
method: method, method: method,
credentials: "include", credentials: "include",
body: JSON.stringify(body),
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
mode: "cors", mode: "cors",
}); };
if (body) {
req["body"] = JSON.stringify(body);
}
return fetch(`${REACT_APP_BACKEND_URL}/${endpoint}`, req);
}; };
export { makeRequest }; const sendLoginRequest = async (
username: string,
password: string
): Promise<object> => {
const p: Promise<object> = new Promise(async (res) => {
await makeRequest({
endpoint: "login",
method: "POST",
body: { username, password },
})
.then((resp) => resp.json())
.then((data) => {
if (data.error) {
res({ isError: true, ...data });
}
res({ isError: false, ...data });
});
res({ isError: true });
});
return p;
};
const sendLogoutRequest = async (): Promise<object> => {
const p: Promise<object> = new Promise(async (res) => {
await makeRequest({
endpoint: "logout",
method: "POST",
})
.then((resp) => resp.json())
.then((data) => {
res({ isError: false, ...data });
});
res({ isError: true });
});
return p;
};
export { makeRequest, sendLoginRequest, sendLogoutRequest };