Repository: Sharvin26/TodoApp
Branch: master
Commit: 114a6d91f8ba
Files: 29
Total size: 57.7 KB
Directory structure:
gitextract_9jnop0m0/
├── .gitignore
├── README.md
├── firebase.json
├── functions/
│ ├── .gitignore
│ ├── APIs/
│ │ ├── todos.js
│ │ └── users.js
│ ├── index.js
│ ├── package.json
│ └── util/
│ ├── admin.js
│ ├── auth.js
│ └── validators.js
└── view/
├── .gitignore
├── README.md
├── package.json
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── robots.txt
└── src/
├── App.css
├── App.js
├── App.test.js
├── components/
│ ├── account.js
│ └── todo.js
├── index.js
├── pages/
│ ├── home.js
│ ├── login.js
│ └── signup.js
├── serviceWorker.js
├── setupTests.js
└── util/
└── auth.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log*
.firebaserc
# Firebase cache
.firebase/
# Firebase config
# Uncomment this if you'd like others to create their own Firebase project.
# For a team working on the same Firebase project(s), it is recommended to leave
# it commented so all members can deploy to the same project(s) in .firebaserc.
# .firebaserc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
functions/package-lock.json
functions/util/config.js
functions/package-lock.json
functions/package-lock.json
functions/package-lock.json
================================================
FILE: README.md
================================================
# Todo Application using ReactJS and Firebase
-----
### Account creation:

### Todo Dashboard:

### Application Architecture:

### Components used in the Application:
1. ReactJS
2. Material UI
3. Firebase Firestore, Functions and Authentication
4. ExpressJS
5. Postman.
================================================
FILE: firebase.json
================================================
{}
================================================
FILE: functions/.gitignore
================================================
node_modules/
================================================
FILE: functions/APIs/todos.js
================================================
const { db } = require('../util/admin');
exports.getAllTodos = (request, response) => {
db
.collection('todos')
.where('username', '==', request.user.username)
.orderBy('createdAt', 'desc')
.get()
.then((data) => {
let todos = [];
data.forEach((doc) => {
todos.push({
todoId: doc.id,
title: doc.data().title,
username: doc.data().username,
body: doc.data().body,
createdAt: doc.data().createdAt,
});
});
return response.json(todos);
})
.catch((err) => {
console.error(err);
return response.status(500).json({ error: err.code});
});
};
exports.getOneTodo = (request, response) => {
db
.doc(`/todos/${request.params.todoId}`)
.get()
.then((doc) => {
if (!doc.exists) {
return response.status(404).json(
{
error: 'Todo not found'
});
}
if(doc.data().username !== request.user.username){
return response.status(403).json({error:"UnAuthorized"})
}
TodoData = doc.data();
TodoData.todoId = doc.id;
return response.json(TodoData);
})
.catch((err) => {
console.error(err);
return response.status(500).json({ error: error.code });
});
};
exports.postOneTodo = (request, response) => {
if (request.body.body.trim() === '') {
return response.status(400).json({ body: 'Must not be empty' });
}
if(request.body.title.trim() === '') {
return response.status(400).json({ title: 'Must not be empty' });
}
const newTodoItem = {
title: request.body.title,
username: request.user.username,
body: request.body.body,
createdAt: new Date().toISOString()
}
db
.collection('todos')
.add(newTodoItem)
.then((doc)=>{
const responseTodoItem = newTodoItem;
responseTodoItem.id = doc.id;
return response.json(responseTodoItem);
})
.catch((error) => {
console.error(error);
response.status(500).json({ error: 'Something went wrong' });
});
};
exports.deleteTodo = (request, response) => {
const document = db.doc(`/todos/${request.params.todoId}`);
document
.get()
.then((doc) => {
if (!doc.exists) {
return response.status(404).json({
error: 'Todo not found'
})}
if(doc.data().username !== request.user.username){
return response.status(403).json({error:"UnAuthorized"})
}
return document.delete();
})
.then(() => {
response.json({ message: 'Delete successfull' });
})
.catch((err) => {
console.error(err);
return response.status(500).json({
error: err.code
});
});
};
exports.editTodo = ( request, response ) => {
if(request.body.todoId || request.body.createdAt){
response.status(403).json({message: 'Not allowed to edit'});
}
let document = db.collection('todos').doc(`${request.params.todoId}`);
document.update(request.body)
.then((doc)=> {
response.json({message: 'Updated successfully'});
})
.catch((error) => {
if(error.code === 5){
response.status(404).json({message: 'Not Found'});
}
console.error(error);
return response.status(500).json({
error: error.code
});
});
};
================================================
FILE: functions/APIs/users.js
================================================
const { admin, db } = require('../util/admin');
const config = require('../util/config');
const firebase = require('firebase');
firebase.initializeApp(config);
const { validateLoginData, validateSignUpData } = require('../util/validators');
// Login
exports.loginUser = (request, response) => {
const user = {
email: request.body.email,
password: request.body.password
}
const { valid, errors } = validateLoginData(user);
if (!valid) return response.status(400).json(errors);
firebase
.auth()
.signInWithEmailAndPassword(user.email, user.password)
.then((data) => {
return data.user.getIdToken();
})
.then((token) => {
return response.json({ token });
})
.catch((error) => {
console.error(error);
return response.status(403).json(
{
general: 'wrong credentials, please try again'
}
);
})
};
// Sign up
exports.signUpUser = (request, response) => {
const newUser = {
firstName: request.body.firstName,
lastName: request.body.lastName,
email: request.body.email,
phoneNumber: request.body.phoneNumber,
country: request.body.country,
password: request.body.password,
confirmPassword: request.body.confirmPassword,
username: request.body.username
};
const { valid, errors } = validateSignUpData(newUser);
if (!valid) return response.status(400).json(errors);
let token, userId;
db
.doc(`/users/${newUser.username}`)
.get()
.then((doc) => {
if (doc.exists) {
return response.status(400).json({ username: 'this username is already taken' });
} else {
return firebase
.auth()
.createUserWithEmailAndPassword(
newUser.email,
newUser.password
);
}
})
.then((data) => {
userId = data.user.uid;
return data.user.getIdToken();
})
.then((idtoken) => {
token = idtoken;
const userCredentials = {
firstName: newUser.firstName,
lastName: newUser.lastName,
username: newUser.username,
phoneNumber: newUser.phoneNumber,
country: newUser.country,
email: newUser.email,
createdAt: new Date().toISOString(),
userId
};
return db
.doc(`/users/${newUser.username}`)
.set(userCredentials);
})
.then(()=>{
return response.status(201).json({ token });
})
.catch((err) => {
console.error(err);
if (err.code === 'auth/email-already-in-use') {
return response.status(400).json({ email: 'Email already in use' });
} else {
return response.status(500).json({ general: 'Something went wrong, please try again' });
}
});
}
deleteImage = (imageName) => {
const bucket = admin.storage().bucket();
const path = `${imageName}`
return bucket.file(path).delete()
.then(() => {
return
})
.catch((error) => {
return
})
}
// Upload profile picture
exports.uploadProfilePhoto = (request, response) => {
const BusBoy = require('busboy');
const path = require('path');
const os = require('os');
const fs = require('fs');
const busboy = new BusBoy({ headers: request.headers });
let imageFileName;
let imageToBeUploaded = {};
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
if (mimetype !== 'image/png' && mimetype !== 'image/jpeg') {
return response.status(400).json({ error: 'Wrong file type submited' });
}
const imageExtension = filename.split('.')[filename.split('.').length - 1];
imageFileName = `${request.user.username}.${imageExtension}`;
const filePath = path.join(os.tmpdir(), imageFileName);
imageToBeUploaded = { filePath, mimetype };
file.pipe(fs.createWriteStream(filePath));
});
deleteImage(imageFileName);
busboy.on('finish', () => {
admin
.storage()
.bucket()
.upload(imageToBeUploaded.filePath, {
resumable: false,
metadata: {
metadata: {
contentType: imageToBeUploaded.mimetype
}
}
})
.then(() => {
const imageUrl = `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}?alt=media`;
return db.doc(`/users/${request.user.username}`).update({
imageUrl
});
})
.then(() => {
return response.json({ message: 'Image uploaded successfully' });
})
.catch((error) => {
console.error(error);
return response.status(500).json({ error: error.code });
});
});
busboy.end(request.rawBody);
};
exports.getUserDetail = (request, response) => {
let userData = {};
db
.doc(`/users/${request.user.username}`)
.get()
.then((doc) => {
if (doc.exists) {
userData.userCredentials = doc.data();
return response.json(userData);
}
})
.catch((error) => {
console.error(error);
return response.status(500).json({ error: error.code });
});
}
exports.updateUserDetails = (request, response) => {
let document = db.collection('users').doc(`${request.user.username}`);
document.update(request.body)
.then(()=> {
response.json({message: 'Updated successfully'});
})
.catch((error) => {
console.error(error);
return response.status(500).json({
message: "Cannot Update the value"
});
});
}
================================================
FILE: functions/index.js
================================================
const functions = require('firebase-functions');
const app = require('express')();
const auth = require('./util/auth');
const {
getAllTodos,
getOneTodo,
postOneTodo,
deleteTodo,
editTodo
} = require('./APIs/todos')
const {
loginUser,
signUpUser,
uploadProfilePhoto,
getUserDetail,
updateUserDetails
} = require('./APIs/users')
// Todos
app.get('/todos', auth, getAllTodos);
app.get('/todo/:todoId', auth, getOneTodo);
app.post('/todo',auth, postOneTodo);
app.delete('/todo/:todoId',auth, deleteTodo);
app.put('/todo/:todoId',auth, editTodo);
// Users
app.post('/login', loginUser);
app.post('/signup', signUpUser);
app.post('/user/image', auth ,uploadProfilePhoto);
app.post('/user', auth ,updateUserDetails);
app.get('/user', auth, getUserDetail);
exports.api = functions.https.onRequest(app);
================================================
FILE: functions/package.json
================================================
{
"name": "functions",
"description": "Cloud Functions for Firebase",
"scripts": {
"serve": "firebase emulators:start --only functions",
"shell": "firebase functions:shell",
"start": "npm run shell",
"deploy": "firebase deploy --only functions",
"logs": "firebase functions:log"
},
"engines": {
"node": "8"
},
"dependencies": {
"busboy": "^0.3.1",
"express": "^4.17.1",
"firebase": "^7.13.1",
"firebase-admin": "^8.10.0",
"firebase-functions": "^3.3.0"
},
"devDependencies": {
"firebase-functions-test": "^0.1.6"
},
"private": true
}
================================================
FILE: functions/util/admin.js
================================================
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
module.exports = { admin, db };
================================================
FILE: functions/util/auth.js
================================================
const { admin, db } = require('./admin');
module.exports = (request, response, next) => {
let idToken;
if (request.headers.authorization && request.headers.authorization.startsWith('Bearer ')) {
idToken = request.headers.authorization.split('Bearer ')[1];
} else {
console.error('No token found');
return response.status(403).json({ error: 'Unauthorized' });
}
admin
.auth()
.verifyIdToken(idToken)
.then((decodedToken) => {
request.user = decodedToken;
return db.collection('users').where('userId', '==', request.user.uid).limit(1).get();
})
.then((data) => {
request.user.username = data.docs[0].data().username;
request.user.imageUrl = data.docs[0].data().imageUrl;
return next();
})
.catch((err) => {
console.error('Error while verifying token', err);
return response.status(403).json(err);
});
};
================================================
FILE: functions/util/validators.js
================================================
const isEmpty = (string) => {
if (string.trim() === '') return true;
else return false;
};
exports.validateLoginData = (data) => {
let errors = {};
if (isEmpty(data.email)) errors.email = 'Must not be empty';
if (isEmpty(data.password)) errors.password = 'Must not be empty';
return {
errors,
valid: Object.keys(errors).length === 0 ? true : false
};
};
const isEmail = (email) => {
const emailRegEx = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (email.match(emailRegEx)) return true;
else return false;
};
exports.validateSignUpData = (data) => {
let errors = {};
if (isEmpty(data.email)) {
errors.email = 'Must not be empty';
} else if (!isEmail(data.email)) {
errors.email = 'Must be valid email address';
}
if (isEmpty(data.firstName)) errors.firstName = 'Must not be empty';
if (isEmpty(data.lastName)) errors.lastName = 'Must not be empty';
if (isEmpty(data.phoneNumber)) errors.phoneNumber = 'Must not be empty';
if (isEmpty(data.country)) errors.country = 'Must not be empty';
if (isEmpty(data.password)) errors.password = 'Must not be empty';
if (data.password !== data.confirmPassword) errors.confirmPassword = 'Passowrds must be the same';
if (isEmpty(data.username)) errors.username = 'Must not be empty';
return {
errors,
valid: Object.keys(errors).length === 0 ? true : false
};
};
================================================
FILE: view/.gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: view/README.md
================================================
================================================
FILE: view/package.json
================================================
{
"name": "view",
"version": "0.1.0",
"private": true,
"dependencies": {
"@material-ui/core": "^4.9.8",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"dayjs": "^1.8.23",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "https://us-central1-todoapp-655c1.cloudfunctions.net/api"
}
================================================
FILE: view/public/index.html
================================================
React App
You need to enable JavaScript to run this app.
================================================
FILE: view/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: view/public/robots.txt
================================================
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
================================================
FILE: view/src/App.css
================================================
html,
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
background-color: rgb(245, 245,245);
}
================================================
FILE: view/src/App.js
================================================
import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { ThemeProvider as MuiThemeProvider } from '@material-ui/core/styles';
import createMuiTheme from '@material-ui/core/styles/createMuiTheme';
import login from './pages/login';
import signup from './pages/signup';
import home from './pages/home';
const theme = createMuiTheme({
palette: {
primary: {
light: '#33c9dc',
main: '#FF5722',
dark: '#d50000',
contrastText: '#fff'
}
}
});
function App() {
return (
);
}
export default App;
================================================
FILE: view/src/App.test.js
================================================
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render( );
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
================================================
FILE: view/src/components/account.js
================================================
import React, { Component } from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import CircularProgress from '@material-ui/core/CircularProgress';
import CloudUploadIcon from '@material-ui/icons/CloudUpload';
import { Card, CardActions, CardContent, Divider, Button, Grid, TextField } from '@material-ui/core';
import clsx from 'clsx';
import axios from 'axios';
import { authMiddleWare } from '../util/auth';
const styles = (theme) => ({
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
toolbar: theme.mixins.toolbar,
root: {},
details: {
display: 'flex'
},
avatar: {
height: 110,
width: 100,
flexShrink: 0,
flexGrow: 0
},
locationText: {
paddingLeft: '15px'
},
buttonProperty: {
position: 'absolute',
top: '50%'
},
uiProgess: {
position: 'fixed',
zIndex: '1000',
height: '31px',
width: '31px',
left: '50%',
top: '35%'
},
progess: {
position: 'absolute'
},
uploadButton: {
marginLeft: '8px',
margin: theme.spacing(1)
},
customError: {
color: 'red',
fontSize: '0.8rem',
marginTop: 10
},
submitButton: {
marginTop: '10px'
}
});
class account extends Component {
constructor(props) {
super(props);
this.state = {
firstName: '',
lastName: '',
email: '',
phoneNumber: '',
username: '',
country: '',
profilePicture: '',
uiLoading: true,
buttonLoading: false,
imageError: ''
};
}
componentWillMount = () => {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.get('/user')
.then((response) => {
console.log(response.data);
this.setState({
firstName: response.data.userCredentials.firstName,
lastName: response.data.userCredentials.lastName,
email: response.data.userCredentials.email,
phoneNumber: response.data.userCredentials.phoneNumber,
country: response.data.userCredentials.country,
username: response.data.userCredentials.username,
uiLoading: false
});
})
.catch((error) => {
if (error.response.status === 403) {
this.props.history.push('/login');
}
console.log(error);
this.setState({ errorMsg: 'Error in retrieving the data' });
});
};
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
handleImageChange = (event) => {
this.setState({
image: event.target.files[0]
});
};
profilePictureHandler = (event) => {
event.preventDefault();
this.setState({
uiLoading: true
});
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
let form_data = new FormData();
form_data.append('image', this.state.image);
form_data.append('content', this.state.content);
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.post('/user/image', form_data, {
headers: {
'content-type': 'multipart/form-data'
}
})
.then(() => {
window.location.reload();
})
.catch((error) => {
if (error.response.status === 403) {
this.props.history.push('/login');
}
console.log(error);
this.setState({
uiLoading: false,
imageError: 'Error in posting the data'
});
});
};
updateFormValues = (event) => {
event.preventDefault();
this.setState({ buttonLoading: true });
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
const formRequest = {
firstName: this.state.firstName,
lastName: this.state.lastName,
country: this.state.country
};
axios
.post('/user', formRequest)
.then(() => {
this.setState({ buttonLoading: false });
})
.catch((error) => {
if (error.response.status === 403) {
this.props.history.push('/login');
}
console.log(error);
this.setState({
buttonLoading: false
});
});
};
render() {
const { classes, ...rest } = this.props;
if (this.state.uiLoading === true) {
return (
{this.state.uiLoading && }
);
} else {
return (
{this.state.firstName} {this.state.lastName}
}
className={classes.uploadButton}
onClick={this.profilePictureHandler}
>
Upload Photo
{this.state.imageError ? (
{' '}
Wrong Image Format || Supported Format are PNG and JPG
) : (
false
)}
Save details
{this.state.buttonLoading && }
);
}
}
}
export default withStyles(styles)(account);
================================================
FILE: view/src/components/todo.js
================================================
import React, { Component } from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import AddCircleIcon from '@material-ui/icons/AddCircle';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import Slide from '@material-ui/core/Slide';
import TextField from '@material-ui/core/TextField';
import Grid from '@material-ui/core/Grid';
import Card from '@material-ui/core/Card';
import CardActions from '@material-ui/core/CardActions';
import CircularProgress from '@material-ui/core/CircularProgress';
import CardContent from '@material-ui/core/CardContent';
import MuiDialogTitle from '@material-ui/core/DialogTitle';
import MuiDialogContent from '@material-ui/core/DialogContent';
import axios from 'axios';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { authMiddleWare } from '../util/auth';
const styles = (theme) => ({
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
appBar: {
position: 'relative'
},
title: {
marginLeft: theme.spacing(2),
flex: 1
},
submitButton: {
display: 'block',
color: 'white',
textAlign: 'center',
position: 'absolute',
top: 14,
right: 10
},
floatingButton: {
position: 'fixed',
bottom: 0,
right: 0
},
form: {
width: '98%',
marginLeft: 13,
marginTop: theme.spacing(3)
},
toolbar: theme.mixins.toolbar,
root: {
minWidth: 470
},
bullet: {
display: 'inline-block',
margin: '0 2px',
transform: 'scale(0.8)'
},
pos: {
marginBottom: 12
},
uiProgess: {
position: 'fixed',
zIndex: '1000',
height: '31px',
width: '31px',
left: '50%',
top: '35%'
},
dialogeStyle: {
maxWidth: '50%'
},
viewRoot: {
margin: 0,
padding: theme.spacing(2)
},
closeButton: {
position: 'absolute',
right: theme.spacing(1),
top: theme.spacing(1),
color: theme.palette.grey[500]
}
});
const Transition = React.forwardRef(function Transition(props, ref) {
return ;
});
class todo extends Component {
constructor(props) {
super(props);
this.state = {
todos: '',
title: '',
body: '',
todoId: '',
errors: [],
open: false,
uiLoading: true,
buttonType: '',
viewOpen: false
};
this.deleteTodoHandler = this.deleteTodoHandler.bind(this);
this.handleEditClickOpen = this.handleEditClickOpen.bind(this);
this.handleViewOpen = this.handleViewOpen.bind(this);
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
componentWillMount = () => {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.get('/todos')
.then((response) => {
this.setState({
todos: response.data,
uiLoading: false
});
})
.catch((err) => {
console.log(err);
});
};
deleteTodoHandler(data) {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
let todoId = data.todo.todoId;
axios
.delete(`todo/${todoId}`)
.then(() => {
window.location.reload();
})
.catch((err) => {
console.log(err);
});
}
handleEditClickOpen(data) {
this.setState({
title: data.todo.title,
body: data.todo.body,
todoId: data.todo.todoId,
buttonType: 'Edit',
open: true
});
}
handleViewOpen(data) {
this.setState({
title: data.todo.title,
body: data.todo.body,
viewOpen: true
});
}
render() {
const DialogTitle = withStyles(styles)((props) => {
const { children, classes, onClose, ...other } = props;
return (
{children}
{onClose ? (
) : null}
);
});
const DialogContent = withStyles((theme) => ({
viewRoot: {
padding: theme.spacing(2)
}
}))(MuiDialogContent);
dayjs.extend(relativeTime);
const { classes } = this.props;
const { open, errors, viewOpen } = this.state;
const handleClickOpen = () => {
this.setState({
todoId: '',
title: '',
body: '',
buttonType: '',
open: true
});
};
const handleSubmit = (event) => {
authMiddleWare(this.props.history);
event.preventDefault();
const userTodo = {
title: this.state.title,
body: this.state.body
};
let options = {};
if (this.state.buttonType === 'Edit') {
options = {
url: `/todo/${this.state.todoId}`,
method: 'put',
data: userTodo
};
} else {
options = {
url: '/todo',
method: 'post',
data: userTodo
};
}
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios(options)
.then(() => {
this.setState({ open: false });
window.location.reload();
})
.catch((error) => {
this.setState({ open: true, errors: error.response.data });
console.log(error);
});
};
const handleViewClose = () => {
this.setState({ viewOpen: false });
};
const handleClose = (event) => {
this.setState({ open: false });
};
if (this.state.uiLoading === true) {
return (
{this.state.uiLoading && }
);
} else {
return (
{this.state.buttonType === 'Edit' ? 'Edit Todo' : 'Create a new Todo'}
{this.state.buttonType === 'Edit' ? 'Save' : 'Submit'}
{this.state.todos.map((todo) => (
{todo.title}
{dayjs(todo.createdAt).fromNow()}
{`${todo.body.substring(0, 65)}`}
this.handleViewOpen({ todo })}>
{' '}
View{' '}
this.handleEditClickOpen({ todo })}>
Edit
this.deleteTodoHandler({ todo })}>
Delete
))}
{this.state.title}
);
}
}
}
export default withStyles(styles)(todo);
================================================
FILE: view/src/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
================================================
FILE: view/src/pages/home.js
================================================
import React, { Component } from 'react';
import axios from 'axios';
import Account from '../components/account';
import Todo from '../components/todo';
import Drawer from '@material-ui/core/Drawer';
import AppBar from '@material-ui/core/AppBar';
import CssBaseline from '@material-ui/core/CssBaseline';
import Toolbar from '@material-ui/core/Toolbar';
import List from '@material-ui/core/List';
import Typography from '@material-ui/core/Typography';
import Divider from '@material-ui/core/Divider';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import withStyles from '@material-ui/core/styles/withStyles';
import AccountBoxIcon from '@material-ui/icons/AccountBox';
import NotesIcon from '@material-ui/icons/Notes';
import Avatar from '@material-ui/core/avatar';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import CircularProgress from '@material-ui/core/CircularProgress';
import { authMiddleWare } from '../util/auth';
const drawerWidth = 240;
const styles = (theme) => ({
root: {
display: 'flex'
},
appBar: {
zIndex: theme.zIndex.drawer + 1
},
drawer: {
width: drawerWidth,
flexShrink: 0
},
drawerPaper: {
width: drawerWidth
},
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
avatar: {
height: 110,
width: 100,
flexShrink: 0,
flexGrow: 0,
marginTop: 20
},
uiProgess: {
position: 'fixed',
zIndex: '1000',
height: '31px',
width: '31px',
left: '45%',
top: '35%'
},
toolbar: theme.mixins.toolbar
});
class home extends Component {
state = {
render: false
};
loadAccountPage = (event) => {
this.setState({ render: true });
};
loadTodoPage = (event) => {
this.setState({ render: false });
};
logoutHandler = (event) => {
localStorage.removeItem('AuthToken');
this.props.history.push('/login');
};
constructor(props) {
super(props);
this.state = {
firstName: '',
lastName: '',
profilePicture: '',
uiLoading: true,
imageLoading: false
};
}
componentWillMount = () => {
authMiddleWare(this.props.history);
const authToken = localStorage.getItem('AuthToken');
axios.defaults.headers.common = { Authorization: `${authToken}` };
axios
.get('/user')
.then((response) => {
console.log(response.data);
this.setState({
firstName: response.data.userCredentials.firstName,
lastName: response.data.userCredentials.lastName,
email: response.data.userCredentials.email,
phoneNumber: response.data.userCredentials.phoneNumber,
country: response.data.userCredentials.country,
username: response.data.userCredentials.username,
uiLoading: false,
profilePicture: response.data.userCredentials.imageUrl
});
})
.catch((error) => {
if (error.response.status === 403) {
this.props.history.push('/login');
}
console.log(error);
this.setState({ errorMsg: 'Error in retrieving the data' });
});
};
render() {
const { classes } = this.props;
if (this.state.uiLoading === true) {
return (
{this.state.uiLoading && }
);
} else {
return (
TodoApp
{' '}
{this.state.firstName} {this.state.lastName}
{' '}
{' '}
{' '}
{' '}
{' '}
{' '}
);
}
}
}
export default withStyles(styles)(home);
================================================
FILE: view/src/pages/login.js
================================================
// Material UI components
import React, { Component } from 'react';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
import Typography from '@material-ui/core/Typography';
import withStyles from '@material-ui/core/styles/withStyles';
import Container from '@material-ui/core/Container';
import CircularProgress from '@material-ui/core/CircularProgress';
import axios from 'axios';
const styles = (theme) => ({
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
form: {
width: '100%',
marginTop: theme.spacing(1)
},
submit: {
margin: theme.spacing(3, 0, 2)
},
customError: {
color: 'red',
fontSize: '0.8rem',
marginTop: 10
},
progess: {
position: 'absolute'
}
});
class login extends Component {
constructor(props) {
super(props);
this.state = {
email: '',
password: '',
errors: [],
loading: false
};
}
componentWillReceiveProps(nextProps) {
if("errors" in nextProps.UI){
if (nextProps.UI.errors) {
this.setState({
errors: nextProps.UI.errors
});
}
}
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({ loading: true });
const userData = {
email: this.state.email,
password: this.state.password
};
axios
.post('/login', userData)
.then((response) => {
localStorage.setItem('AuthToken', `Bearer ${response.data.token}`);
this.setState({
loading: false
});
this.props.history.push('/');
})
.catch((error) => {
this.setState({
errors: error.response.data,
loading: false
});
});
};
render() {
const { classes } = this.props;
const { errors, loading } = this.state;
return (
);
}
}
export default withStyles(styles)(login);
================================================
FILE: view/src/pages/signup.js
================================================
import React, { Component } from 'react';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
import Typography from '@material-ui/core/Typography';
import Container from '@material-ui/core/Container';
import withStyles from '@material-ui/core/styles/withStyles';
import CircularProgress from '@material-ui/core/CircularProgress';
import axios from 'axios';
const styles = (theme) => ({
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(3)
},
submit: {
margin: theme.spacing(3, 0, 2)
},
progess: {
position: 'absolute'
}
});
class signup extends Component {
constructor(props) {
super(props);
this.state = {
firstName: '',
lastName: '',
phoneNumber: '',
country: '',
username: '',
email: '',
password: '',
confirmPassword: '',
errors: [],
loading: false
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.UI.errors) {
this.setState({
errors: nextProps.UI.errors
});
}
}
handleChange = (event) => {
this.setState({
[event.target.name]: event.target.value
});
};
handleSubmit = (event) => {
event.preventDefault();
this.setState({ loading: true });
const newUserData = {
firstName: this.state.firstName,
lastName: this.state.lastName,
phoneNumber: this.state.phoneNumber,
country: this.state.country,
username: this.state.username,
email: this.state.email,
password: this.state.password,
confirmPassword: this.state.confirmPassword
};
axios
.post('/signup', newUserData)
.then((response) => {
localStorage.setItem('AuthToken', `Bearer ${response.data.token}`);
this.setState({
loading: false,
});
this.props.history.push('/');
})
.catch((error) => {
this.setState({
errors: error.response.data,
loading: false
});
});
};
render() {
const { classes } = this.props;
const { errors, loading } = this.state;
return (
);
}
}
export default withStyles(styles)(signup);
================================================
FILE: view/src/serviceWorker.js
================================================
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}
================================================
FILE: view/src/setupTests.js
================================================
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';
================================================
FILE: view/src/util/auth.js
================================================
export const authMiddleWare = (history) => {
const authToken = localStorage.getItem('AuthToken');
if(authToken === null){
history.push('/login')
}
}