diff --git a/frontend/README.md b/frontend/README.md index 64e343e1846df7c27fe49505a338146e8aeb189f..176c1cdc37b1efdfd84180e916730ecc69e6348c 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,6 +1,30 @@ This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). -## Available Scripts +## Setup + +First, run `yarn install` to install the node modules used for the frontend. + +In order for the API links to work, the following repository branches need to be cloned and run separately: + +### 1. materials@scientia-integration +```shell +# Clone the repository and checkout to the relevant branch +git clone https://gitlab.doc.ic.ac.uk/edtech/materials.git +git checkout scientia-integration + +# Setup and run the development server on port 5000 +python3 -m venv venv +source venv/bin/activate +pip install --upgrade pip && pip install -r requirements.txt +export FLASK_ENV=development +export FLASK_APP=materials +flask create_all +flask populate -r materials/mocks/resources.json +flask run +``` + + +## To run In the project directory, you can run: diff --git a/frontend/src/components/pages/ModuleList/index.tsx b/frontend/src/components/pages/ModuleList/index.tsx index 0a4fde6f4cb0393fb11c739a64577fe5f81b4bd8..93ea4322e4756202e9122a22b98635ed553351e7 100644 --- a/frontend/src/components/pages/ModuleList/index.tsx +++ b/frontend/src/components/pages/ModuleList/index.tsx @@ -29,7 +29,7 @@ const ModuleList: React.FC = () => { }, { title: "Discrete Mathematics", - code: "CO142", + code: "142", image: discreteIllustration, terms: [Term.AUTUMN], progressStatus: ProgressStatus.IN_PROGRESS, diff --git a/frontend/src/components/pages/ModuleResources/index.tsx b/frontend/src/components/pages/ModuleResources/index.tsx index a1a93203c895bf861fa752d4d35b5895ff3110c4..0c075b4b04d42f846b68b72765bde755ce7177fb 100644 --- a/frontend/src/components/pages/ModuleResources/index.tsx +++ b/frontend/src/components/pages/ModuleResources/index.tsx @@ -1,5 +1,9 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import styles from "./style.module.scss"; + +import classNames from "classnames"; +import { request } from "../../../utils/api" +import { api } from "../../../constants/routes" import MyBreadcrumbs from "components/atoms/MyBreadcrumbs"; import InputGroup from "react-bootstrap/InputGroup"; diff --git a/frontend/src/components/pages/ModuleResources/style.module.scss b/frontend/src/components/pages/ModuleResources/style.module.scss index 1ece388a13e426c13b6f64524dd498766d796667..c0e45a84077da719e17ebd16b93bb433ae0e964b 100644 --- a/frontend/src/components/pages/ModuleResources/style.module.scss +++ b/frontend/src/components/pages/ModuleResources/style.module.scss @@ -51,4 +51,4 @@ font-weight: 500 !important; color: $white !important; border-color: $black; -} \ No newline at end of file +} diff --git a/frontend/src/components/pages/StandardView/index.tsx b/frontend/src/components/pages/StandardView/index.tsx index 4f40d2ca508fc437dce7f6727f213ed04dfb58e6..64a3e72bfb6851271fb0fc8d50561b8c8a7d236b 100644 --- a/frontend/src/components/pages/StandardView/index.tsx +++ b/frontend/src/components/pages/StandardView/index.tsx @@ -70,9 +70,7 @@ const StandardView: React.FC<StandardViewProps> = ({ <ModuleResources /> </Route> - <Route path="/modules/:id/feedback"> - <ModuleFeedback /> - </Route> + <Route path="/modules/:id/feedback" component={ModuleFeedback} /> <Route path="/timeline"> <ExamplePage name="Timeline" /> diff --git a/frontend/src/constants/auth.tsx b/frontend/src/constants/auth.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e0ef8c55ea1e05b05fbf3e708831fab3f7ed4f33 --- /dev/null +++ b/frontend/src/constants/auth.tsx @@ -0,0 +1,8 @@ +const authConstants = { + ACCESS_TOKEN_HEADER: () => `Bearer ${sessionStorage.getItem("currentUser")}`, + ACCESS_TOKEN: "currentUser", + USER_INFO: "userInfo" +}; + +export default authConstants; + \ No newline at end of file diff --git a/frontend/src/constants/routes.tsx b/frontend/src/constants/routes.tsx new file mode 100644 index 0000000000000000000000000000000000000000..dd37f6e245d5e0516af3a876b3a631900a99b536 --- /dev/null +++ b/frontend/src/constants/routes.tsx @@ -0,0 +1,15 @@ +const dev = { + MATERIALS_URL: "http://localhost:5000" +} + +const prod = { + MATERIALS_URL: "https://materials.doc.ic.ac.uk", +} + +const config = process.env.NODE_ENV === "development" ? dev : prod; + +export const api = { + MATERIALS_LOGIN: config.MATERIALS_URL + "/auth/login", + MATERIALS_COURSES: config.MATERIALS_URL + "/courses/1819", + MATERIALS_RESOURCES: config.MATERIALS_URL + "/resources" +} \ No newline at end of file diff --git a/frontend/src/utils/api.tsx b/frontend/src/utils/api.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b21d81177ec0c48f5daa7c51ac1f72f2f25990f3 --- /dev/null +++ b/frontend/src/utils/api.tsx @@ -0,0 +1,41 @@ +import authConstants from "../constants/auth"; +import authenticationService from "../utils/auth"; +import { api } from "../constants/routes" + +interface RequestOptions { + [key: string]: any +} + +// API calling interface. onSuccess and onError are functions that take in data +// and error parameters respectively. Body is process as query parameters if +// method is GET +// Note: will trigger CORS OPTIONS preflight due to the Authorization header +export async function request(url: string, method: string, onSuccess: any, onError: any, body?: any) { + if (!authenticationService.userIsLoggedIn()) { + // TODO: Credentials should be handled elsewhere + // TODO: Specific endpoint login route should be passed in + await authenticationService.login("abc123", "a", api.MATERIALS_LOGIN); + } + + var options: RequestOptions = { + method: method, + mode: "cors", + headers: { + "Authorization": authConstants.ACCESS_TOKEN_HEADER(), + "Access-Control-Allow-Origin": "*", + }, + }; + + if (method === "GET") { + url = url + "?" + new URLSearchParams(body); + } else { + options.body = JSON.stringify(body); + } + + return fetch(url, options) + .then((result) => { + onSuccess(result); + }, (error) => { + onError(error); + }) +} diff --git a/frontend/src/utils/auth.tsx b/frontend/src/utils/auth.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b0e19a20f1444aeff6cdf42faef8f747ad6a78ca --- /dev/null +++ b/frontend/src/utils/auth.tsx @@ -0,0 +1,52 @@ +import authConstants from "../constants/auth"; + +function storeDataInStorage(data: { access_token: string; user_info: any; }) { + sessionStorage.setItem(authConstants.ACCESS_TOKEN, data.access_token); + sessionStorage.setItem( + authConstants.USER_INFO, + JSON.stringify(data.user_info) + ); +} + +function removeDataFromStorage() { + sessionStorage.removeItem(authConstants.ACCESS_TOKEN); + sessionStorage.removeItem(authConstants.USER_INFO); +} + +function getUserInfo() { + const info = sessionStorage.getItem(authConstants.USER_INFO) + if (info) { + return JSON.parse(info) + } + return {} +} + +async function login(username: string, password: string, login_url: string) { + const response = await fetch(login_url, { + method: "POST", + mode: "cors", + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + body: JSON.stringify({ username, password }) + }); + if (response.ok) { + const data = await response.json(); + storeDataInStorage(data); + return true; + } + return false; +} + +const logout = () => removeDataFromStorage(); +const userIsLoggedIn = () => { + return sessionStorage.getItem(authConstants.ACCESS_TOKEN) !== null; +}; + +export default { + login, + logout, + userIsLoggedIn, + getUserInfo +};