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
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
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`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
================================================
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 (
<MuiThemeProvider theme={theme}>
<Router>
<div>
<Switch>
<Route exact path="/" component={home} />
<Route exact path="/login" component={login} />
<Route exact path="/signup" component={signup} />
</Switch>
</div>
</Router>
</MuiThemeProvider>
);
}
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(<App />);
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 (
<main className={classes.content}>
<div className={classes.toolbar} />
{this.state.uiLoading && <CircularProgress size={150} className={classes.uiProgess} />}
</main>
);
} else {
return (
<main className={classes.content}>
<div className={classes.toolbar} />
<Card {...rest} className={clsx(classes.root, classes)}>
<CardContent>
<div className={classes.details}>
<div>
<Typography className={classes.locationText} gutterBottom variant="h4">
{this.state.firstName} {this.state.lastName}
</Typography>
<Button
variant="outlined"
color="primary"
type="submit"
size="small"
startIcon={<CloudUploadIcon />}
className={classes.uploadButton}
onClick={this.profilePictureHandler}
>
Upload Photo
</Button>
<input type="file" onChange={this.handleImageChange} />
{this.state.imageError ? (
<div className={classes.customError}>
{' '}
Wrong Image Format || Supported Format are PNG and JPG
</div>
) : (
false
)}
</div>
</div>
<div className={classes.progress} />
</CardContent>
<Divider />
</Card>
<br />
<Card {...rest} className={clsx(classes.root, classes)}>
<form autoComplete="off" noValidate>
<Divider />
<CardContent>
<Grid container spacing={3}>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="First name"
margin="dense"
name="firstName"
variant="outlined"
value={this.state.firstName}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Last name"
margin="dense"
name="lastName"
variant="outlined"
value={this.state.lastName}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Email"
margin="dense"
name="email"
variant="outlined"
disabled={true}
value={this.state.email}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Phone Number"
margin="dense"
name="phone"
type="number"
variant="outlined"
disabled={true}
value={this.state.phoneNumber}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="User Name"
margin="dense"
name="userHandle"
disabled={true}
variant="outlined"
value={this.state.username}
onChange={this.handleChange}
/>
</Grid>
<Grid item md={6} xs={12}>
<TextField
fullWidth
label="Country"
margin="dense"
name="country"
variant="outlined"
value={this.state.country}
onChange={this.handleChange}
/>
</Grid>
</Grid>
</CardContent>
<Divider />
<CardActions />
</form>
</Card>
<Button
color="primary"
variant="contained"
type="submit"
className={classes.submitButton}
onClick={this.updateFormValues}
disabled={
this.state.buttonLoading ||
!this.state.firstName ||
!this.state.lastName ||
!this.state.country
}
>
Save details
{this.state.buttonLoading && <CircularProgress size={30} className={classes.progess} />}
</Button>
</main>
);
}
}
}
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 <Slide direction="up" ref={ref} {...props} />;
});
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 (
<MuiDialogTitle disableTypography className={classes.root} {...other}>
<Typography variant="h6">{children}</Typography>
{onClose ? (
<IconButton aria-label="close" className={classes.closeButton} onClick={onClose}>
<CloseIcon />
</IconButton>
) : null}
</MuiDialogTitle>
);
});
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 (
<main className={classes.content}>
<div className={classes.toolbar} />
{this.state.uiLoading && <CircularProgress size={150} className={classes.uiProgess} />}
</main>
);
} else {
return (
<main className={classes.content}>
<div className={classes.toolbar} />
<IconButton
className={classes.floatingButton}
color="primary"
aria-label="Add Todo"
onClick={handleClickOpen}
>
<AddCircleIcon style={{ fontSize: 60 }} />
</IconButton>
<Dialog fullScreen open={open} onClose={handleClose} TransitionComponent={Transition}>
<AppBar className={classes.appBar}>
<Toolbar>
<IconButton edge="start" color="inherit" onClick={handleClose} aria-label="close">
<CloseIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
{this.state.buttonType === 'Edit' ? 'Edit Todo' : 'Create a new Todo'}
</Typography>
<Button
autoFocus
color="inherit"
onClick={handleSubmit}
className={classes.submitButton}
>
{this.state.buttonType === 'Edit' ? 'Save' : 'Submit'}
</Button>
</Toolbar>
</AppBar>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="todoTitle"
label="Todo Title"
name="title"
autoComplete="todoTitle"
helperText={errors.title}
value={this.state.title}
error={errors.title ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="todoDetails"
label="Todo Details"
name="body"
autoComplete="todoDetails"
multiline
rows={25}
rowsMax={25}
helperText={errors.body}
error={errors.body ? true : false}
onChange={this.handleChange}
value={this.state.body}
/>
</Grid>
</Grid>
</form>
</Dialog>
<Grid container spacing={2}>
{this.state.todos.map((todo) => (
<Grid item xs={12} sm={6}>
<Card className={classes.root} variant="outlined">
<CardContent>
<Typography variant="h5" component="h2">
{todo.title}
</Typography>
<Typography className={classes.pos} color="textSecondary">
{dayjs(todo.createdAt).fromNow()}
</Typography>
<Typography variant="body2" component="p">
{`${todo.body.substring(0, 65)}`}
</Typography>
</CardContent>
<CardActions>
<Button size="small" color="primary" onClick={() => this.handleViewOpen({ todo })}>
{' '}
View{' '}
</Button>
<Button size="small" color="primary" onClick={() => this.handleEditClickOpen({ todo })}>
Edit
</Button>
<Button size="small" color="primary" onClick={() => this.deleteTodoHandler({ todo })}>
Delete
</Button>
</CardActions>
</Card>
</Grid>
))}
</Grid>
<Dialog
onClose={handleViewClose}
aria-labelledby="customized-dialog-title"
open={viewOpen}
fullWidth
classes={{ paperFullWidth: classes.dialogeStyle }}
>
<DialogTitle id="customized-dialog-title" onClose={handleViewClose}>
{this.state.title}
</DialogTitle>
<DialogContent dividers>
<TextField
fullWidth
id="todoDetails"
name="body"
multiline
readonly
rows={1}
rowsMax={25}
value={this.state.body}
InputProps={{
disableUnderline: true
}}
/>
</DialogContent>
</Dialog>
</main>
);
}
}
}
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(
<React.StrictMode>
<App />
</React.StrictMode>,
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 (
<div className={classes.root}>
{this.state.uiLoading && <CircularProgress size={150} className={classes.uiProgess} />}
</div>
);
} else {
return (
<div className={classes.root}>
<CssBaseline />
<AppBar position="fixed" className={classes.appBar}>
<Toolbar>
<Typography variant="h6" noWrap>
TodoApp
</Typography>
</Toolbar>
</AppBar>
<Drawer
className={classes.drawer}
variant="permanent"
classes={{
paper: classes.drawerPaper
}}
>
<div className={classes.toolbar} />
<Divider />
<center>
<Avatar src={this.state.profilePicture} className={classes.avatar} />
<p>
{' '}
{this.state.firstName} {this.state.lastName}
</p>
</center>
<Divider />
<List>
<ListItem button key="Todo" onClick={this.loadTodoPage}>
<ListItemIcon>
{' '}
<NotesIcon />{' '}
</ListItemIcon>
<ListItemText primary="Todo" />
</ListItem>
<ListItem button key="Account" onClick={this.loadAccountPage}>
<ListItemIcon>
{' '}
<AccountBoxIcon />{' '}
</ListItemIcon>
<ListItemText primary="Account" />
</ListItem>
<ListItem button key="Logout" onClick={this.logoutHandler}>
<ListItemIcon>
{' '}
<ExitToAppIcon />{' '}
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItem>
</List>
</Drawer>
<div>{this.state.render ? <Account /> : <Todo />}</div>
</div>
);
}
}
}
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 (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Login
</Typography>
<form className={classes.form} noValidate>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
helperText={errors.email}
error={errors.email ? true : false}
onChange={this.handleChange}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
helperText={errors.password}
error={errors.password ? true : false}
onChange={this.handleChange}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={this.handleSubmit}
disabled={loading || !this.state.email || !this.state.password}
>
Sign In
{loading && <CircularProgress size={30} className={classes.progess} />}
</Button>
<Grid container>
<Grid item>
<Link href="signup" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
{errors.general && (
<Typography variant="body2" className={classes.customError}>
{errors.general}
</Typography>
)}
</form>
</div>
</Container>
);
}
}
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 (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="firstName"
label="First Name"
name="firstName"
autoComplete="firstName"
helperText={errors.firstName}
error={errors.firstName ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="lastName"
label="Last Name"
name="lastName"
autoComplete="lastName"
helperText={errors.lastName}
error={errors.lastName ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="username"
label="User Name"
name="username"
autoComplete="username"
helperText={errors.username}
error={errors.username ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="phoneNumber"
label="Phone Number"
name="phoneNumber"
autoComplete="phoneNumber"
pattern="[7-9]{1}[0-9]{9}"
helperText={errors.phoneNumber}
error={errors.phoneNumber ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
helperText={errors.email}
error={errors.email ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="country"
label="Country"
name="country"
autoComplete="country"
helperText={errors.country}
error={errors.country ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
helperText={errors.password}
error={errors.password ? true : false}
onChange={this.handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="confirmPassword"
label="Confirm Password"
type="password"
id="confirmPassword"
autoComplete="current-password"
onChange={this.handleChange}
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
onClick={this.handleSubmit}
disabled={loading ||
!this.state.email ||
!this.state.password ||
!this.state.firstName ||
!this.state.lastName ||
!this.state.country ||
!this.state.username ||
!this.state.phoneNumber}
>
Sign Up
{loading && <CircularProgress size={30} className={classes.progess} />}
</Button>
<Grid container justify="flex-end">
<Grid item>
<Link href="login" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</form>
</div>
</Container>
);
}
}
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')
}
}
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
SYMBOL INDEX (25 symbols across 7 files)
FILE: view/src/App.js
function App (line 20) | function App() {
FILE: view/src/components/account.js
class account (line 62) | class account extends Component {
method constructor (line 63) | constructor(props) {
method render (line 178) | render() {
FILE: view/src/components/todo.js
class todo (line 96) | class todo extends Component {
method constructor (line 97) | constructor(props) {
method deleteTodoHandler (line 140) | deleteTodoHandler(data) {
method handleEditClickOpen (line 155) | handleEditClickOpen(data) {
method handleViewOpen (line 165) | handleViewOpen(data) {
method render (line 173) | render() {
FILE: view/src/pages/home.js
class home (line 64) | class home extends Component {
method constructor (line 82) | constructor(props) {
method render (line 122) | render() {
FILE: view/src/pages/login.js
class login (line 45) | class login extends Component {
method constructor (line 46) | constructor(props) {
method componentWillReceiveProps (line 57) | componentWillReceiveProps(nextProps) {
method render (line 98) | render() {
FILE: view/src/pages/signup.js
class signup (line 39) | class signup extends Component {
method constructor (line 40) | constructor(props) {
method componentWillReceiveProps (line 57) | componentWillReceiveProps(nextProps) {
method render (line 101) | render() {
FILE: view/src/serviceWorker.js
function register (line 23) | function register(config) {
function registerValidSW (line 57) | function registerValidSW(swUrl, config) {
function checkValidServiceWorker (line 101) | function checkValidServiceWorker(swUrl, config) {
function unregister (line 131) | function unregister() {
Condensed preview — 29 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (69K chars).
[
{
"path": ".gitignore",
"chars": 1293,
"preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nfirebase-debug.log*\n.firebaserc\n\n# Firebase cache\n.fire"
},
{
"path": "README.md",
"chars": 395,
"preview": "# Todo Application using ReactJS and Firebase\n\n-----\n\n### Account creation:\n\n\n\n### Tod"
},
{
"path": "firebase.json",
"chars": 3,
"preview": "{}\n"
},
{
"path": "functions/.gitignore",
"chars": 13,
"preview": "node_modules/"
},
{
"path": "functions/APIs/todos.js",
"chars": 3573,
"preview": "const { db } = require('../util/admin');\n\nexports.getAllTodos = (request, response) => {\n\tdb\n .collection('todos'"
},
{
"path": "functions/APIs/users.js",
"chars": 5741,
"preview": "const { admin, db } = require('../util/admin');\nconst config = require('../util/config');\n\nconst firebase = require('fir"
},
{
"path": "functions/index.js",
"chars": 841,
"preview": "const functions = require('firebase-functions');\nconst app = require('express')();\nconst auth = require('./util/auth');\n"
},
{
"path": "functions/package.json",
"chars": 604,
"preview": "{\n \"name\": \"functions\",\n \"description\": \"Cloud Functions for Firebase\",\n \"scripts\": {\n \"serve\": \"firebase emulator"
},
{
"path": "functions/util/admin.js",
"chars": 128,
"preview": "const admin = require('firebase-admin');\n\nadmin.initializeApp();\n\nconst db = admin.firestore();\n\nmodule.exports = { admi"
},
{
"path": "functions/util/auth.js",
"chars": 851,
"preview": "const { admin, db } = require('./admin');\n\nmodule.exports = (request, response, next) => {\n\tlet idToken;\n\tif (request.he"
},
{
"path": "functions/util/validators.js",
"chars": 1453,
"preview": "const isEmpty = (string) => {\n\tif (string.trim() === '') return true;\n\telse return false;\n};\n\nexports.validateLoginData "
},
{
"path": "view/.gitignore",
"chars": 310,
"preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
},
{
"path": "view/README.md",
"chars": 0,
"preview": ""
},
{
"path": "view/package.json",
"chars": 870,
"preview": "{\n \"name\": \"view\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"@material-ui/core\": \"^4.9.8\",\n "
},
{
"path": "view/public/index.html",
"chars": 1537,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-w"
},
{
"path": "view/public/manifest.json",
"chars": 492,
"preview": "{\n \"short_name\": \"React App\",\n \"name\": \"Create React App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n "
},
{
"path": "view/public/robots.txt",
"chars": 67,
"preview": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
},
{
"path": "view/src/App.css",
"chars": 227,
"preview": "html,\nbody {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarel"
},
{
"path": "view/src/App.js",
"chars": 854,
"preview": "import React from 'react';\nimport { BrowserRouter as Router, Route, Switch } from 'react-router-dom';\nimport { ThemeProv"
},
{
"path": "view/src/App.test.js",
"chars": 280,
"preview": "import React from 'react';\nimport { render } from '@testing-library/react';\nimport App from './App';\n\ntest('renders lear"
},
{
"path": "view/src/components/account.js",
"chars": 8178,
"preview": "import React, { Component } from 'react';\n\nimport withStyles from '@material-ui/core/styles/withStyles';\nimport Typograp"
},
{
"path": "view/src/components/todo.js",
"chars": 9768,
"preview": "import React, { Component } from 'react';\n\nimport withStyles from '@material-ui/core/styles/withStyles';\nimport Typograp"
},
{
"path": "view/src/index.js",
"chars": 483,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport App from './App';\nimport * as serviceWorker from '."
},
{
"path": "view/src/pages/home.js",
"chars": 4770,
"preview": "import React, { Component } from 'react';\nimport axios from 'axios';\n\nimport Account from '../components/account';\nimpor"
},
{
"path": "view/src/pages/login.js",
"chars": 3977,
"preview": "// Material UI components\nimport React, { Component } from 'react';\nimport Avatar from '@material-ui/core/Avatar';\nimpor"
},
{
"path": "view/src/pages/signup.js",
"chars": 6834,
"preview": "import React, { Component } from 'react';\nimport Avatar from '@material-ui/core/Avatar';\nimport Button from '@material-u"
},
{
"path": "view/src/serviceWorker.js",
"chars": 5086,
"preview": "// This optional code is used to register a service worker.\n// register() is not called by default.\n\n// This lets the ap"
},
{
"path": "view/src/setupTests.js",
"chars": 255,
"preview": "// jest-dom adds custom jest matchers for asserting on DOM nodes.\n// allows you to do things like:\n// expect(element).to"
},
{
"path": "view/src/util/auth.js",
"chars": 168,
"preview": "export const authMiddleWare = (history) => {\n const authToken = localStorage.getItem('AuthToken');\n if(authToken ="
}
]
About this extraction
This page contains the full source code of the Sharvin26/TodoApp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 29 files (57.7 KB), approximately 15.4k tokens, and a symbol index with 25 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.