Skip to content

Commit

Permalink
add server interaction
Browse files Browse the repository at this point in the history
  • Loading branch information
luigidr committed May 23, 2021
1 parent c680367 commit d6deacf
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 38 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ It will be developed in phases, in several weeks, to illustrate the different fe
* _Phase 1_: just static component rendering, and props-propagation. Uses 'fake' data and does not allow any user interaction (except one).
* _Phase 2_: add a form and its management.
* _Phase 3_: edit form and client-side routing. The application now has 3 different pages.
* _Phase 4_: add interaction with the server for all the operations. Basic error handling.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"proxy": "http://localhost:3001",
"eslintConfig": {
"extends": [
"react-app",
Expand Down
94 changes: 94 additions & 0 deletions src/API.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* All the API calls
*/
import Course from './models/Course';
import Exam from './models/Exam';

const BASEURL = '/api';

async function getAllCourses() {
// call: GET /api/courses
const response = await fetch(BASEURL + '/courses');
const coursesJson = await response.json();
if (response.ok) {
return coursesJson.map((co) => Course.from(co));
} else {
throw coursesJson; // an object with the error coming from the server
}
}

async function getAllExams() {
// call: GET /api/exams
const response = await fetch(BASEURL + '/exams');
const examsJson = await response.json();
if (response.ok) {
return examsJson.map((ex) => Exam.from(ex));
} else {
throw examsJson; // an object with the error coming from the server
}
}

function deleteExam(coursecode) {
// call: DELETE /api/exams/:coursecode
return new Promise((resolve, reject) => {
fetch(BASEURL + '/exams/' + coursecode, {
method: 'DELETE',
}).then((response) => {
if (response.ok) {
resolve(null);
} else {
// analyze the cause of error
response.json()
.then((message) => { reject(message); }) // error message in the response body
.catch(() => { reject({ error: "Cannot parse server response." }) }); // something else
}
}).catch(() => { reject({ error: "Cannot communicate with the server." }) }); // connection errors
});
}

function addExam(exam) {
// call: POST /api/exams
return new Promise((resolve, reject) => {
fetch(BASEURL + '/exams', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({code: exam.coursecode, score: exam.score, date: exam.date}),
}).then((response) => {
if (response.ok) {
resolve(null);
} else {
// analyze the cause of error
response.json()
.then((message) => { reject(message); }) // error message in the response body
.catch(() => { reject({ error: "Cannot parse server response." }) }); // something else
}
}).catch(() => { reject({ error: "Cannot communicate with the server." }) }); // connection errors
});
}

function updateExam(exam) {
// call: PUT /api/exams/:coursecode
return new Promise((resolve, reject) => {
fetch(BASEURL + '/exams/' + exam.coursecode, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({code: exam.coursecode, score: exam.score, date: exam.date}),
}).then((response) => {
if (response.ok) {
resolve(null);
} else {
// analyze the cause of error
response.json()
.then((obj) => { reject(obj); }) // error message in the response body
.catch(() => { reject({ error: "Cannot parse server response." }) }); // something else
}
}).catch(() => { reject({ error: "Cannot communicate with the server." }) }); // connection errors
});
}

const API = {getAllCourses, getAllExams, deleteExam, addExam, updateExam};
export default API;
148 changes: 115 additions & 33 deletions src/App.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,131 @@
import 'bootstrap/dist/css/bootstrap.min.css';
import './App.css';
import { Container, Row } from 'react-bootstrap';
import {ExamScores, ExamForm} from './ExamComponents.js';
import { Container, Row, Alert } from 'react-bootstrap';
import { ExamScores, ExamForm } from './ExamComponents.js';
import AppTitle from './AppTitle.js';
import dayjs from 'dayjs';
import { useState } from 'react';
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom';

const fakeExams = [
{coursecode: '01TYMOV', score: 28, date: dayjs('2021-03-01')},
{coursecode: '01SQJOV', score: 29, date: dayjs('2021-06-03')},
{coursecode: '04GSPOV', score: 18, date: dayjs('2021-05-24')},
{coursecode: '01TXYOV', score: 24, date: dayjs('2021-06-21')},
];

const fakeCourses = [
{coursecode: '01TYMOV', name: 'Information systems security'},
{coursecode: '02LSEOV', name: 'Computer architectures'},
{coursecode: '01SQJOV', name: 'Data Science and Database Technology'},
{coursecode: '01OTWOV', name: 'Computer network technologies and services'},
{coursecode: '04GSPOV', name: 'Software Engineering'},
{coursecode: '01TXYOV', name: 'Web Applications I'},
{coursecode: '01NYHOV', name: 'System and device programming'},
{coursecode: '01TYDOV', name: 'Cloud Computing'},
{coursecode: '01SQPOV', name: 'Software Networking'},
];
import { useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import API from './API';

function App() {
const [exams, setExams] = useState([...fakeExams]);
const [exams, setExams] = useState([]);
const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
const [dirty, setDirty] = useState(true);
const [errorMsg, setErrorMsg] = useState('');

const examCodes = exams.map(exam => exam.coursecode) ;
useEffect(()=> {
const getCourses = async () => {
const courses = await API.getAllCourses();
setCourses(courses);
};
getCourses()
.catch(err => {
setErrorMsg("Impossible to load your exams! Please, try again later...");
console.error(err);
});;
}, []);

useEffect(()=> {
const getExams = async () => {
const exams = await API.getAllExams();
setExams(exams);
};
if(courses.length && dirty) {
getExams().then(() => {
setLoading(false);
setDirty(false);
}).catch(err => {
setErrorMsg("Impossible to load your exams! Please, try again later...");
console.error(err);
});;
}
}, [courses.length, dirty]);

/* With requests in parallel...
useEffect(() => {
const promises = [API.getAllCourses(), API.getAllExams()];
Promise.all(promises).then(
([cs, ex]) => {
setCourses(cs);
setExams(ex);
setLoading(false);
}
).catch(err => {
setErrorMsg("Impossible to load your exams! Please, try again later...");
console.error(err);
});
}, []);
useEffect(()=> {
const getExams = async () => {
const exams = await API.getAllExams();
setExams(exams);
};
if(courses.length && dirty) {
getExams().then(() => {
setDirty(false);
}).catch(err => {
setErrorMsg("Impossible to load your exams! Please, try again later...");
console.error(err);
});
}
}, [dirty]); */

const handleErrors = (err) => {
if(err.errors)
setErrorMsg(err.errors[0].msg + ': ' + err.errors[0].param);
else
setErrorMsg(err.error);

setDirty(true);
}

const deleteExam = (coursecode) => {
setExams((exs) => exs.filter(ex => ex.coursecode !== coursecode))
// temporary set the deleted item as "in progress"
setExams(oldExams => {
return oldExams.map(ex => {
if (ex.coursecode === coursecode)
return {...ex, status: 'deleted'};
else
return ex;
});
});

API.deleteExam(coursecode)
.then(() => {
setDirty(true);
}).catch(err => handleErrors(err) );
}

const addExam = (exam) => {
exam.status = 'added';
setExams(oldExams => [...oldExams, exam]);

API.addExam(exam)
.then(() => {
setDirty(true);
}).catch(err => handleErrors(err) );
}

const updateExam = (exam) => {
setExams(oldExams => {
return oldExams.map(ex => {
if (ex.coursecode === exam.coursecode)
return {coursecode: exam.coursecode, score: exam.score, date: exam.date};
return {coursecode: exam.coursecode, score: exam.score, date: exam.date, status: 'updated'};
else
return ex;
});
});

API.updateExam(exam)
.then(() => {
setDirty(true);
}).catch(err => handleErrors(err) );;
}

const examCodes = exams.map(exam => exam.coursecode);

return (<Router>
<Container className="App">
<Row>
Expand All @@ -58,23 +134,29 @@ function App() {

<Switch>
<Route path="/add" render={() =>
<ExamForm courses={fakeCourses.filter(course => !examCodes.includes(course.coursecode))} addOrUpdateExam={addExam}></ExamForm>
<ExamForm courses={courses.filter(course => !examCodes.includes(course.coursecode))} addOrUpdateExam={addExam}></ExamForm>
}/>

{/* without useLocation():
<Route path="/update" render={(routeProps) =>
<ExamForm courses={fakeCourses} exam={routeProps.location.state.exam} examDate={routeProps.location.state.examDate} addOrUpdateExam={updateExam}></ExamForm>
<ExamForm courses={courses} exam={routeProps.location.state.exam} examDate={routeProps.location.state.examDate} addOrUpdateExam={updateExam}></ExamForm>
}/>
*/}
{/* with useLocation() in ExamForm */}
<Route path="/update" render={() =>
<ExamForm courses={fakeCourses} addOrUpdateExam={updateExam}></ExamForm>
<ExamForm courses={courses} addOrUpdateExam={updateExam}></ExamForm>
}/>

<Route path="/" render={() =>
<Route path="/" render={() =>
<>
<Row>
{errorMsg && <Alert variant='danger' onClose={() => setErrorMsg('')} dismissible>{errorMsg}</Alert>}
</Row>
<Row>
<ExamScores exams={exams} courses={fakeCourses} deleteExam={deleteExam}/>
{loading ? <span>🕗 Please wait, loading your exams... 🕗</span> :
<ExamScores exams={exams} courses={courses} deleteExam={deleteExam}/> }
</Row>
</>
} />

</Switch>
Expand Down
28 changes: 23 additions & 5 deletions src/ExamComponents.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,40 @@ function ExamTable(props) {
}

function ExamRow(props) {
return <tr><ExamRowData exam={props.exam} examName={props.examName} /><RowControls exam={props.exam} deleteExam={props.deleteExam} /></tr>
let statusClass = null;

switch(props.exam.status) {
case 'added':
statusClass = 'table-success';
break;
case 'deleted':
statusClass = 'table-danger';
break;
case 'updated':
statusClass = 'table-warning';
break;
default:
break;
}

return <tr className={statusClass}><ExamRowData exam={props.exam} examName={props.examName} />{ !props.exam.status ? <RowControls exam={props.exam} deleteExam={props.deleteExam} />: <td></td>}</tr>
}

function ExamRowData(props) {


return <>
<td>{props.examName}</td>
<td>{props.exam.score}</td>
<td>{props.exam.date.format('DD MMM YYYY')}</td>
<td>{dayjs(props.exam.date).format('DD MMM YYYY')}</td>
</>;
}

function RowControls(props) {
return <td>
<Link to={{
pathname: "/update",
state: { exam: props.exam, examDate: props.exam.date.format('YYYY-MM-DD') }
state: { exam: props.exam }
}}>{iconEdit}
</Link> <span onClick={() => { props.deleteExam(props.exam.coursecode) }}>{iconDelete}</span>
</td>
Expand All @@ -66,13 +84,13 @@ function ExamForm(props) {
const location = useLocation();
const [course, setCourse] = useState(location.state ? location.state.exam.coursecode : '');
const [score, setScore] = useState(location.state ? location.state.exam.score : 30);
const [date, setDate] = useState(location.state ? location.state.examDate : dayjs().format('YYYY-MM-DD'));
const [date, setDate] = useState(location.state ? location.state.exam.date : dayjs().format('YYYY-MM-DD'));
const [errorMessage, setErrorMessage] = useState('') ;
const [submitted, setSubmitted] = useState(false);

const handleSubmit = (event) => {
event.preventDefault();
const exam = { coursecode: course, score: score, date: dayjs(date) };
const exam = { coursecode: course, score: score, date: dayjs(date).format('YYYY-MM-DD') };

// SOME VALIDATION, ADD MORE!!!
let valid = true;
Expand Down
31 changes: 31 additions & 0 deletions src/models/Course.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Object describing a course
*/
class Course {
/**
* Create a new Course
* @param {*} coursecode unique code for the course
* @param {*} name full name of the course
* @param {*} CFU number of CFU credits
*/
constructor(coursecode, name, CFU) {
this.coursecode = coursecode;
this.name = name;
this.CFU = CFU;
}

/**
* Creates a new Course from plain (JSON) objects
* @param {*} json a plain object (coming form JSON deserialization)
* with the right properties
* @return {Course} the newly created object
*/
static from(json) {
const course = new Course();
delete Object.assign(course, json, {coursecode: json.code}).code;
return course;
}

}

export default Course;
Loading

0 comments on commit d6deacf

Please sign in to comment.