diff --git a/src/front/component/App.jsx b/src/front/component/App.jsx index ac741af..bcc1ec2 100644 --- a/src/front/component/App.jsx +++ b/src/front/component/App.jsx @@ -1,52 +1,188 @@ +/* eslint-disable react/forbid-prop-types */ import * as React from 'react'; -import { BrowserRouter as Router } from "react-router-dom"; - +import { withRouter } from "react-router-dom"; +import PropTypes from 'prop-types'; // ES6 import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; +import ButtonGroup from '@material-ui/core/ButtonGroup'; +import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown'; +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import Grow from '@material-ui/core/Grow'; +import Paper from '@material-ui/core/Paper'; +import Popper from '@material-ui/core/Popper'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@material-ui/core/MenuList'; + +import IconButton from '@material-ui/core/IconButton'; +import ArrowBack from '@material-ui/icons/ArrowBack'; + import { logout, connectedState } from '../services/auth'; import Routes from "./Router"; +const options = ['Documentation','My Profile']; + class App extends React.PureComponent { constructor() { super(); - this.state = {userConnected : false}; + this.state = { + canBack: false, + userConnected : false, + anchorRef: {current: null}, + selectedIndex: 0, + open: false + }; } componentDidMount() { + const { history } = this.props; + if (history.location.pathname !== '/home' && history.location.pathname !== '/') { + this.setState({canBack: true}); + } + history.listen((location) => { + if (location.pathname !== '/home' && location.pathname !== '/') { + this.setState({canBack: true}); + } else { + this.setState({canBack: false}); + } + }); + + connectedState.subscribe((userConnected) => { + if(!userConnected) { + history.push('/'); + } this.setState({userConnected}); + }); + + } + + async handleMenuItemClick(event, index) { + const { history } = this.props; + if (index === 1) { + history.push('/profile') + } + + this.handleToggle() + + } + + handleToggle() { + const { open } = this.state; + this.setState({ + open: !open }) } + handleClose(event) { + const { anchorRef } = this.state; + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + + this.setState({ + open: false + }) + } + + goBack() { + const { history } = this.props; + history.goBack(); + } + + render() { - const { userConnected } = this.state; + const { userConnected, anchorRef, selectedIndex, open, canBack } = this.state; return (
+ {canBack && ( + {this.goBack()}} + > + + + )} + Connect
- {userConnected && } - + {userConnected && + ( + <> + + + + + + {({ TransitionProps, placement }) => ( + + + this.handleClose()}> + + {options.map((option, index) => ( + this.handleMenuItemClick(event, index)} + > + {option} + + ))} + + + + + )} + + + ) + } - - - +
); } } -export default App; +App.propTypes = { + history: PropTypes.instanceOf(Object).isRequired, +}; + + + +export default withRouter(App); diff --git a/src/front/component/Router.jsx b/src/front/component/Router.jsx index 2a94226..e20d4d9 100644 --- a/src/front/component/Router.jsx +++ b/src/front/component/Router.jsx @@ -5,6 +5,7 @@ import LoginPage from '../pages/login/login'; import Github from '../pages/login/github'; import HomePage from '../pages/home/home'; import DetailsPage from '../pages/details/details'; +import ProfilePage from '../pages/profile/profile'; export const ROUTES = { HOME: '/', @@ -28,6 +29,12 @@ class Routes extends React.PureComponent { component={DetailsPage} /> + + - - ); } diff --git a/src/front/index.jsx b/src/front/index.jsx index b217674..f715198 100644 --- a/src/front/index.jsx +++ b/src/front/index.jsx @@ -1,8 +1,10 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { BrowserRouter } from "react-router-dom"; + import App from './component/App'; import './index.css'; import registerServiceWorker from './registerServiceWorker'; -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render(, document.getElementById('root')); registerServiceWorker(); diff --git a/src/front/pages/details/details.jsx b/src/front/pages/details/details.jsx index 88dc339..de6bc72 100644 --- a/src/front/pages/details/details.jsx +++ b/src/front/pages/details/details.jsx @@ -1,13 +1,19 @@ -/* eslint-disable react/forbid-prop-types */ +/* eslint-disable no-underscore-dangle */ import * as React from 'react'; import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; import TextField from '@material-ui/core/TextField'; import CircularProgress from '@material-ui/core/CircularProgress'; - +import Button from '@material-ui/core/Button'; +import { green } from '@material-ui/core/colors'; +import InputAdornment from '@material-ui/core/InputAdornment'; +import IconButton from '@material-ui/core/IconButton'; +import FileCopy from '@material-ui/icons/FileCopy'; import PropTypes from 'prop-types'; // ES6 +import Snackbar from '@material-ui/core/Snackbar'; -import { getApplication } from '../../services/api'; +import { getApplication, updateApplication } from '../../services/api'; +import { validateFormField, checkValid } from '../../services/formValidator'; const styles = { @@ -17,6 +23,7 @@ const styles = { margin: "0 auto", display: 'flex', flexWrap: 'wrap', + "margin-top": 16 }, progress: { margin: "0 auto", @@ -27,7 +34,30 @@ const styles = { }, textField: { margin: 16 - } + }, + button: { + margin: 8, + width: 120 + }, + buttonContainer: { + display: "flex", + width: "100%", + "justify-content": "flex-end", + }, + buttonProgress: { + color: green[500], + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, + wrapper: { + margin: 0, + position: 'relative', + }, + + }; @@ -36,15 +66,14 @@ class DetailsPage extends React.PureComponent { super(); this.state = { loading: true, - application: { - name: "Name", - description: "Description", - token: "token", - token_sandbox: "token sandbox", - apple_store_link: "http://apple", - google_market_link: "http://google", - create_at: new Date(), - updated_at: new Date(), + updateLoading: false, + application: {}, + snackBarOpen: false, + errors: { + name: false, + description: false, + apple_store_link: false, + google_market_link: false, } } } @@ -52,26 +81,80 @@ class DetailsPage extends React.PureComponent { componentDidMount() { const { match } = this.props; getApplication(match.params.appId).then((res) => { - this.setState({ - loading: false, - application: res + this.setState((prevState) => { + return { + ...prevState, + loading: false, + application: res + } }); }); } handleChange(name, event) { - const { application } = this.state; + const { application, errors } = this.state; + const { value } = event.target; + + const validated = validateFormField(value, name); this.setState({ application: { ...application, - [name]: event.target.value - } + [name]: value + }, + errors: { + ...errors, + [name]: !validated + } + }); + } + + handleClick() { + this.setState({ + snackBarOpen: true + }); + } + + handleClose(event, reason) { + if (reason === 'clickaway') { + return; + } + + this.setState({ + snackBarOpen: false + }); + } + + + goBack() { + const { history } = this.props; + history.goBack(); + } + + async clickUpdateApplication() { + const { application } = this.state; + this.setState({ + updateLoading: true + }) + const response = await updateApplication(application._id, application); + this.setState({ + updateLoading: false, + application: response + }) + } + + copyToClipboard(key) { + const { application } = this.state; + navigator.permissions.query({name: "clipboard-write"}).then(result => { + if (result.state === "granted" || result.state === "prompt") { + navigator.clipboard.writeText(application[key]); + this.setState({snackBarOpen: true}); + } }); } render() { const { classes } = this.props; - const { application, loading } = this.state; + const { application, errors, loading, updateLoading, snackBarOpen } = this.state; return ( <>
@@ -89,6 +172,7 @@ class DetailsPage extends React.PureComponent { onChange={(event) => this.handleChange('name', event)} margin="normal" variant="outlined" + error={errors.name} /> @@ -102,55 +186,99 @@ class DetailsPage extends React.PureComponent { margin="normal" variant="outlined" multiline - rows="4" + rows="4" + error={errors.name} /> this.handleChange('token', event)} + value={application.apple_store_link || ''} + onChange={(event) => this.handleChange('apple_store_link', event)} margin="normal" variant="outlined" + error={errors.apple_store_link} /> - this.handleChange('token_sandbox', event)} + value={application.google_market_link || ''} + onChange={(event) => this.handleChange('google_market_link', event)} margin="normal" variant="outlined" + error={errors.google_market_link} /> +
+
+ + {updateLoading && } +
+
+ this.handleChange('apple_store_link', event)} + value={application.token} margin="normal" variant="outlined" + InputProps={{ + endAdornment: ( + + {this.copyToClipboard('token')}}> + + + + ) + }} /> this.handleChange('google_market_link', event)} + value={application.token_sandbox} margin="normal" variant="outlined" + InputProps={{ + endAdornment: ( + + {this.copyToClipboard('token_sandbox')}}> + + + + ) + }} + /> + + {this.handleClose()}} + message={Copied!} /> @@ -164,9 +292,9 @@ class DetailsPage extends React.PureComponent { } DetailsPage.propTypes = { - classes: PropTypes.object.isRequired, - // history: PropTypes.object.isRequired, - match: PropTypes.object.isRequired + classes: PropTypes.instanceOf(Object).isRequired, + history: PropTypes.instanceOf(Object).isRequired, + match: PropTypes.instanceOf(Object).isRequired }; diff --git a/src/front/pages/home/home.jsx b/src/front/pages/home/home.jsx index ae6804e..242f914 100644 --- a/src/front/pages/home/home.jsx +++ b/src/front/pages/home/home.jsx @@ -1,5 +1,4 @@ /* eslint-disable no-underscore-dangle */ -/* eslint-disable react/forbid-prop-types */ import * as React from 'react'; import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; @@ -10,8 +9,8 @@ import ListItemText from '@material-ui/core/ListItemText'; import Typography from '@material-ui/core/Typography'; import CircularProgress from '@material-ui/core/CircularProgress'; import Fab from '@material-ui/core/Fab'; -import IconButton from '@material-ui/core/IconButton'; import AddIcon from '@material-ui/icons/Add'; +import DeveloperModeIcon from '@material-ui/icons/DeveloperMode'; import Button from '@material-ui/core/Button'; import TextField from '@material-ui/core/TextField'; @@ -20,12 +19,16 @@ import DialogActions from '@material-ui/core/DialogActions'; import DialogContent from '@material-ui/core/DialogContent'; import DialogContentText from '@material-ui/core/DialogContentText'; import DialogTitle from '@material-ui/core/DialogTitle'; +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; import Moment from 'react-moment'; import PropTypes from 'prop-types'; // ES6 import { listOfApplications, createApplication } from '../../services/api'; +import { validateFormField, checkValid } from '../../services/formValidator'; const styles = { @@ -55,6 +58,12 @@ const styles = { marginRight: 16, }, + card: { + maxWidth: 720, + margin: "0 auto", + marginTop: 120 + } + }; @@ -70,6 +79,12 @@ class HomePage extends React.PureComponent { description: '', apple_store_link: '', google_market_link: '' + }, + errors: { + name: false, + description: false, + apple_store_link: false, + google_market_link: false, } } } @@ -102,17 +117,32 @@ class HomePage extends React.PureComponent { description: '', apple_store_link: '', google_market_link: '' + }, + errors: { + name: false, + description: false, + apple_store_link: false, + google_market_link: false, } }); } + + handleChange(name, event) { - const { newApplication } = this.state; + const { newApplication, errors } = this.state; + const { value } = event.target; + + const validated = validateFormField(value, name); this.setState({ newApplication: { ...newApplication, - [name]: event.target.value - } + [name]: value + }, + errors: { + ...errors, + [name]: !validated + } }); } @@ -127,7 +157,7 @@ class HomePage extends React.PureComponent { render() { const { classes } = this.props; - const { developerApplications, loading, dialogNewApplicationOpen, newApplication } = this.state; + const { developerApplications, loading, dialogNewApplicationOpen, newApplication, errors } = this.state; return ( <> @@ -166,6 +196,27 @@ class HomePage extends React.PureComponent { } + { + developerApplications.length === 0 && !loading && + ( + + + + + {" "} Welcome + + + Add your first application with the bottom right button. + + + + + + + + ) + } + this.handleChange('name', event)} margin="normal" variant="outlined" + error={errors.name} /> @@ -210,7 +262,8 @@ class HomePage extends React.PureComponent { margin="normal" variant="outlined" multiline - rows="4" + rows="4" + error={errors.description} /> this.handleChange('apple_store_link', event)} margin="normal" variant="outlined" + error={errors.apple_store_link} /> @@ -234,6 +288,7 @@ class HomePage extends React.PureComponent { onChange={(event) => this.handleChange('google_market_link', event)} margin="normal" variant="outlined" + error={errors.google_market_link} /> @@ -241,7 +296,7 @@ class HomePage extends React.PureComponent { - @@ -253,8 +308,8 @@ class HomePage extends React.PureComponent { } HomePage.propTypes = { - classes: PropTypes.object.isRequired, - history: PropTypes.object.isRequired, + classes: PropTypes.instanceOf(Object).isRequired, + history: PropTypes.instanceOf(Object).isRequired, }; diff --git a/src/front/pages/login/github.jsx b/src/front/pages/login/github.jsx index 189e704..02fe149 100644 --- a/src/front/pages/login/github.jsx +++ b/src/front/pages/login/github.jsx @@ -30,7 +30,7 @@ class Github extends React.Component { setAuthToken(await responses.text()); - return history.push('/'); + return history.push('/home'); } render() { diff --git a/src/front/pages/login/login.jsx b/src/front/pages/login/login.jsx index e5c2f4e..bdc150f 100644 --- a/src/front/pages/login/login.jsx +++ b/src/front/pages/login/login.jsx @@ -1,5 +1,3 @@ -/* eslint-disable react/forbid-prop-types */ - import { withRouter } from 'react-router-dom'; import { withStyles } from '@material-ui/core/styles'; import Card from '@material-ui/core/Card'; @@ -73,8 +71,8 @@ class LoginPage extends React.PureComponent { } LoginPage.propTypes = { - history: PropTypes.object.isRequired, - classes: PropTypes.object.isRequired, + classes: PropTypes.instanceOf(Object).isRequired, + history: PropTypes.instanceOf(Object).isRequired, }; diff --git a/src/front/pages/profile/profile.jsx b/src/front/pages/profile/profile.jsx new file mode 100644 index 0000000..8db9a8b --- /dev/null +++ b/src/front/pages/profile/profile.jsx @@ -0,0 +1,210 @@ +/* eslint-disable no-underscore-dangle */ +import * as React from 'react'; +import { withRouter } from 'react-router-dom'; +import { withStyles } from '@material-ui/core/styles'; +import TextField from '@material-ui/core/TextField'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Button from '@material-ui/core/Button'; +import { green } from '@material-ui/core/colors'; + +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; + +import PropTypes from 'prop-types'; // ES6 + +import { updateUser, deleteUser } from '../../services/api'; +import { getUser } from '../../services/auth'; + + +const styles = { + root: { + width: '100%', + maxWidth: 720, + margin: "0 auto", + display: 'flex', + flexWrap: 'wrap', + "margin-top": 16 + }, + progress: { + margin: "0 auto", + "margin-top": 140 + }, + listContainer: { + width: "100%" + }, + textField: { + margin: 16 + }, + button: { + margin: 8, + width: 180 + }, + buttonContainer: { + display: "flex", + width: "100%", + "justify-content": "flex-end", + }, + buttonProgress: { + color: green[500], + position: 'absolute', + top: '50%', + left: '50%', + marginTop: -12, + marginLeft: -12, + }, + wrapper: { + margin: 0, + position: 'relative', + }, + nameContainer: { + display: 'flex', + width: "100%", + } + + +}; + + +class ProfilePage extends React.PureComponent { + constructor() { + super(); + this.state = { + loading: true, + updateLoading: false, + user: {}, + dialogOpen: false, + } + } + + componentDidMount() { + const user = getUser(); + this.setState({ + loading: false, + user + }); + } + + goBack() { + const { history } = this.props; + history.goBack(); + } + + async clickUpdateApplication() { + const { application } = this.state; + this.setState({ + updateLoading: true + }) + const response = await updateUser(application._id, application); + this.setState({ + updateLoading: false, + user: response + }) + } + + confirmationDialog() { + this.setState({ + dialogOpen: true + }); + } + + async handleClose(userToBeDeleted) { + if (userToBeDeleted){ + await deleteUser(); + const { history } = this.props; + history.push('/'); + } else { + this.setState({ + dialogOpen: false + }); + } + } + + render() { + const { classes } = this.props; + const { user, loading, updateLoading, dialogOpen } = this.state; + return ( + <> +
+ {loading && } + + {!loading && + ( + <> +
+ this.handleChange('name', event)} + margin="normal" + variant="outlined" + /> + + this.handleChange('githubLogin', event)} + margin="normal" + variant="outlined" + /> +
+ +
+ +
+ + + ) + } +
+ + this.handleClose(false)} + > + Delete your profile? + + + Your profile will be deleted forever and your applications will stop working. + + + + + + + + + + + ); + } +} + +ProfilePage.propTypes = { + classes: PropTypes.instanceOf(Object).isRequired, + history: PropTypes.instanceOf(Object).isRequired, +}; + + +export default withRouter(withStyles(styles)(ProfilePage)); diff --git a/src/front/services/api.jsx b/src/front/services/api.jsx index 42720d5..430070e 100644 --- a/src/front/services/api.jsx +++ b/src/front/services/api.jsx @@ -1,62 +1,113 @@ -import { getJwt } from './auth'; +import { getJwt, logout } from './auth'; -export const listOfApplications = async () => { +const headersWithJWT = () => { const jwt = getJwt(); - if (!jwt) { - return []; - } + if (!jwt) { return null; } + return { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${jwt}` + }; +} + + +export const listOfApplications = async () => { + const headers = headersWithJWT(); + if (!headers) { return []; } const responses = await fetch(`${process.env.API_URL}/api/application`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${jwt}` - }, + headers, method: 'GET', }); - - return responses.json(); } export const getApplication = async (appId) => { - const jwt = getJwt(); - if (!jwt) { - return {}; - } + const headers = headersWithJWT(); + if (!headers) { return {}; } const responses = await fetch(`${process.env.API_URL}/api/application/${appId}`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${jwt}` - }, + headers, method: 'GET', }); - - return responses.json(); } export const createApplication = async(newApplication) => { - const jwt = getJwt(); - if (!jwt) { - return {}; - } + const headers = headersWithJWT(); + if (!headers) { return {}; } const responses = await fetch(`${process.env.API_URL}/api/application`, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${jwt}` - }, + headers, method: 'POST', body: JSON.stringify(newApplication) }); + return responses.json(); +} + +export const updateApplication = async(appId, application) => { + const headers = headersWithJWT(); + if (!headers) { return {}; } + + const responses = await fetch(`${process.env.API_URL}/api/application/${appId}`, { + headers, + method: 'PUT', + body: JSON.stringify(application) + }); return responses.json(); -} \ No newline at end of file +} + +export const deleteUser = async() => { + const headers = headersWithJWT(); + if (!headers) { return {}; } + + const responses = await fetch(`${process.env.API_URL}/api/developer`, { + headers, + method: 'DELETE', + }); + + logout(); + return responses.json(); +} + +export const getUser = async () => { + const headers = headersWithJWT(); + if (!headers) { return {}; } + + // const responses = await fetch(`${process.env.API_URL}/api/application/${appId}`, { + // headers, + // method: 'GET', + // }); + // return responses.json(); + + return { + lastName: 'Bernos', + firstName: 'Guillaume', + email: 'guillaume.bernos@matters.tech', + } +} + +export const updateUser = async () => { + const headers = headersWithJWT(); + if (!headers) { return {}; } + + // const responses = await fetch(`${process.env.API_URL}/api/application/${appId}`, { + // headers, + // method: 'GET', + // }); + // return responses.json(); + + // TODO: WAITING FOR BACKEND + + return { + lastName: 'Bernos', + firstName: 'Guillaume', + email: 'guillaume.bernos@matters.tech', + } +} + diff --git a/src/front/services/auth.jsx b/src/front/services/auth.jsx index 38b8a60..1217eb0 100644 --- a/src/front/services/auth.jsx +++ b/src/front/services/auth.jsx @@ -52,6 +52,17 @@ export const getJwt = () => { return valid; }; +export const getUser = () => { + const jwt = getJwt(); + try { + const decoded = jwtDecode(jwt); + return decoded; + } catch (e) { + return null; + } + +} + export const hasJwt = () => !!getJwt(); hasJwt(); diff --git a/src/front/services/formValidator.jsx b/src/front/services/formValidator.jsx new file mode 100644 index 0000000..22b7462 --- /dev/null +++ b/src/front/services/formValidator.jsx @@ -0,0 +1,41 @@ +export const validateFormField = (value, field) => { + let validated = false; + const urlRegex = new RegExp(/[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)?/gi); + switch(field) { + case 'name': + case 'description': + if (value.length === 0) { + validated = false; + } else { + validated = true; + } + break; + + case 'apple_store_link': + case 'google_market_link': + if (value.match(urlRegex) || value.length === 0) { + validated = true; + } else { + validated = false; + } + break; + + + default: + validated = false; + break; + } + + return validated; +} + +export const checkValid = (errorState) => { + let valid = true + Object.keys(errorState).forEach(element => { + if (errorState[element] === true) { + valid = false; + } + }) + + return valid; +} \ No newline at end of file