18
loading...
This website collects cookies to deliver better user experience
mkdir Appwrite-ExpenseTrack
cd Appwrite-ExpenseTrack
docker run -it --rm \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:0.8.0
docker run -it --rm ^
--volume //var/run/docker.sock:/var/run/docker.sock ^
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
--entrypoint="install" ^
appwrite/appwrite:0.8.0
docker run -it --rm ,
--volume /var/run/docker.sock:/var/run/docker.sock ,
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw ,
--entrypoint="install" ,
appwrite/appwrite:0.8.0
Web App
option.npx create-react-app expense-tracker
cd expense-tracker
yarn add appwrite react-router-dom @material-ui/core @material-ui/icons
.env
file in the root of your React project and paste the following:REACT_APP_APPWRITE_PROJECT=YOUR_PROJECT_ID
REACT_APP_APPWRITE_ENDPOINT=YOUR_APPWRITE_ENDPOINT
YOUR_PROJECT_ID
and YOUR_APPWRITE_ENDPOINT
with your actual project ID and API Endpoint, respectively. These values are found on the project settings page. services
in the src
folder of the React project, and in it, create a file called AppwriteService.js
. Add the following class, which, at the moment, only initializes the SDK. We will be adding more methods to this class.import { Appwrite } from 'appwrite';
const config = {
projectId: process.env.REACT_APP_APPWRITE_PROJECT,
endpoint: process.env.REACT_APP_APPWRITE_ENDPOINT,
};
const appwrite = new Appwrite();
class AppwriteService {
constructor() {
appwrite.setEndpoint(config.endpoint).setProject(config.projectId);
}
}
export default AppwriteService;
src/services/AppwriteService.js
:account
property responsible for handling Auth API calls...
class AppwriteService {
constructor() {
appwrite.setEndpoint(config.endpoint).setProject(config.projectId);
this.account = appwrite.account; // <--- Add account property
}
doCreateAccount = (email, password, name) => {
return this.account.create(email, password, name);
}
doLogin = (email, password) => {
return this.account.createSession(email, password);
}
doLogout = () => {
return this.account.deleteSession('current');
}
}
export default AppwriteService;
src/constants/routes.js
.export const LANDING = '/';
export const SIGN_UP = '/signup';
export const SIGN_IN = '/signin';
export const HOME = '/home';
src/pages/Auth/SignUp.jsx
src/pages/Auth/SignIn.jsx
src/pages/Home/index.jsx
src/pages/Landing/index.jsx
src/pages/Auth/SignUp.jsx
import React from 'react';
const SignUp = () => (
<div>
<h1>SignUp</h1>
</div>
);
export default SignUp;
SignIn
, Home
and Landing
src/components/Navigation.js
and add the following code:import {
AppBar,
Button,
makeStyles,
Toolbar,
Typography,
} from '@material-ui/core';
import React from 'react';
import { useHistory } from 'react-router-dom';
import * as ROUTES from '../constants/routes';
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
title: "{"
flexGrow: 1,
},
}));
export default function Navigation() {
const classes = useStyles();
const history = useHistory();
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<Typography variant="h6" className={classes.title}>
<Link
color="inherit"
href="#"
underline="none"
onClick={(e) => {
e.preventDefault();
history.push(ROUTES.LANDING);
}}
>
Expense Tracker
</Link>
</Typography>
<Button color="inherit" onClick={() => history.push(ROUTES.HOME)}>
Home
</Button>
<Button color="inherit" onClick={() => history.push(ROUTES.SIGN_UP)}>
Sign Up
</Button>
<Button color="inherit" onClick={() => history.push(ROUTES.SIGN_IN)}>
Sign In
</Button>
</Toolbar>
</AppBar>
</div>
);
}
App
component (src/App.js
) to specify which components (pages) should show up according to the corresponding routes, using React Router. We are also going to include our Navigation component to help users navigate the app. Replace the code in App.js
with the following:import { BrowserRouter as Router, Route } from 'react-router-dom';
import Navigation from './components/Navigation';
import * as ROUTES from './constants/routes';
import Landing from './pages/Landing';
import SignUp from './pages/Auth/SignUp';
import SignIn from './pages/Auth/SignIn';
import Home from './pages/Home';
import { Container } from '@material-ui/core';
function App() {
return (
<Router>
<div>
<Navigation />
<Container>
<Route exact path={ROUTES.LANDING} component={Landing} />
<Route exact path={ROUTES.SIGN_UP} component={SignUp} />
<Route exact path={ROUTES.SIGN_IN} component={SignIn} />
<Route exact path={ROUTES.HOME} component={Home} />
</Container>
</div>
</Router>
);
}
export default App;
yarn start
, you should see something like this:src/context/Appwrite/index.js
in your React project and add the following:import React from 'react';
const AppwriteContext = React.createContext(null);
export default AppwriteContext;
src/components/Appwrite/index.js
which exports the AppwriteService
class and AppwriteContext
.import AppwriteContext from '../../context/Appwrite';
import Appwrite from '../../services/AppwriteService';
export default Appwrite;
export { AppwriteContext };
React.createContext()
method in src/context/Appwrite/index.js
creates two components, AppwriteContext.Provider
which is used to provide an Appwrite instance once at the top of our component tree and AppwriteContext.Consumer
for every component which requires access to Appwrite.AppwriteContext.Provider
component to provide an Appwrite instance to the entire application by wrapping it around our root component in /src/index.js
, like this:import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Appwrite, {AppwriteContext} from './components/Appwrite';
ReactDOM.render(
<React.StrictMode>
<AppwriteContext.Provider value={new Appwrite()}>
<App />
</AppwriteContext.Provider>
</React.StrictMode>,
document.getElementById('root')
);
reportWebVitals();
AppwriteContext.Consumer
component. An example of such a component would look like this:import React from 'react';
import {AppwriteContext} from './components/Appwrite';
const SomeExampleComponent = () => (
<AppwriteContext.Consumer>
{appwrite => {
return <div>This component has access to Appwrite.</div>;
}}
</AppwriteContext.Consumer>
);
export default SomeExampleComponent;
useContext()
function. The example above can be rewritten as follows:import React, {useContext} from 'react';
import {AppwriteContext} from './components/Appwrite';
const SomeExampleComponent = () => {
const appwrite = useContext(AppwriteContext);
return (
<div>This component has access to Appwrite.</div>
);
}
export default SomeExampleComponent;
src/pages/Auth/SignUp.js
with the following. Note: This design is based on Material UI templates (with a few modifications), specifically the Sign-Up template (check out the demo and source code)import React from 'react';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';
const useStyles = makeStyles((theme) => ({
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(3),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
export default function SignUp() {
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
autoComplete="name"
name="name"
variant="outlined"
required
fullWidth
id="name"
label="Full Name"
autoFocus
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Sign Up
</Button>
<Grid container justify="flex-end">
<Grid item>
<Link href="#" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</form>
</div>
</Container>
);
}
import React, { useState } from 'react';
...
export default function SignUp() {
const classes = useStyles();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
...
onChange
event listeners to update their values. Below is a snippet of how the TextField
for the name
form field should look. Do the same for the email
and password
fields. You can refer to this commit for what the final edit should look like....
<TextField
autoComplete="name"
name="name"
value={name} // <--- bind to set variable
onChange={(e) => setName(e.target.value)} // <--- implement event handler
variant="outlined"
required
fullWidth
id="name"
label="Full Name"
autoFocus
/>
...
onSubmit
event listener which will call the doCreateAccount
function of the Appwrite service. We are also going to make sure the form doesn't submit empty data by adding some simple validation code. We first need to import the Appwrite context and "consume" it using the useContext
hook. Doing this will give our component access to the AppwriteService
class.import React, { useContext, useState } from 'react'; // Import useContext hook
import { AppwriteContext } from "../../components/Appwrite"; // Import Appwrite context
...
export default function SignUp() {
const classes = useStyles();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// Get Appwrite instance
const appwrite = useContext(AppwriteContext);
// Create event listener
const onSubmit = (e) => {
e.preventDefault();
if (name === '' || email === '' || password === '') {
alert('All fields are required');
return;
}
appwrite.doCreateAccount(email, password, name).then((result) => {
console.log('Success', result);
}).catch((error) => {
console.log('Error', error);
});
}
...
// Bind event listener
<form className={classes.form} noValidate onSubmit={onSubmit}>
...
useContext
to access our Appwrite instance (as described in the previous section, titled Provide Appwrite in React).src/pages/Auth/SignIn.js
, we'll use the following design:import React from 'react';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';
const useStyles = makeStyles((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(1),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
export default function SignIn() {
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<div className={classes.paper}>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<form className={classes.form} noValidate>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Sign In
</Button>
<Grid container>
<Grid item xs>
<Link href="#" variant="body2">
Forgot password?
</Link>
</Grid>
<Grid item>
<Link href="#" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</form>
</div>
</Container>
);
}
import React, { useState } from 'react';
...
export default function SignIn() {
const classes = useStyles();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
...
onChange
event listeners to update their values. Here is a snippet of the email
TextField
. Remember to make the same changes to the password
field. Refer to this commit....
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
value={email} // <-- bind to variable
onChange={(e) => setEmail(e.target.value)} // <-- event handler
autoComplete="email"
autoFocus
/>
onSubmit
event handle, as we did with the sign-up form.import React, { useContext, useState } from 'react'; // Import useContext hook
import { AppwriteContext } from "../../components/Appwrite"; // Import Appwrite context
...
export default function SignIn() {
const classes = useStyles();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
// Get Appwrite instance
const appwrite = useContext(AppwriteContext);
// Create event listener
const onSubmit = (e) => {
e.preventDefault();
if ( email === '' || password === '') {
alert('All fields are required');
return;
}
appwrite.doLogin(email, password).then((result) => {
console.log('Success', result);
}).catch((error) => {
console.log('Error', error);
});
}
...
// Bind event listener
<form className={classes.form} noValidate onSubmit={onSubmit}>
...
src/context/Session/index.js
import React from 'react';
const AuthUserContext = React.createContext({});
export default AuthUserContext
src/services/AppwriteService.js
...
doGetCurrentUser = () => {
return this.account.get();
}
...
src/components/Session/withAuthentication.jsx
, which will contain all the functionality which deals with the authenticated user. We will use AuthUserContext.Provider
to pass the following to consuming components: authUser
(the current user object) and getCurrentUser()
(a method which will be used to get an updated user object).import React, { useCallback, useContext, useEffect, useState } from 'react';
import AuthUserContext from '../../context/Session';
import { AppwriteContext } from '../../components/Appwrite';
const withAuthentication = (Component) =>
function WithAuthentication(props) {
const [authUser, setAuthUser] = useState(null);
const appwrite = useContext(AppwriteContext);
const getCurrentUser = useCallback(() => {
appwrite
.doGetCurrentUser()
.then((user) => {
setAuthUser(user);
})
.catch(() => {
setAuthUser(null);
});
}, [appwrite])
useEffect(() => {
getCurrentUser();
}, [getCurrentUser]);
return (
<AuthUserContext.Provider value={{ authUser, getCurrentUser }}>
<Component {...props} />
</AuthUserContext.Provider>
);
};
export default withAuthentication;
src/components/Session/index.js
import withAuthentication from "./withAuthentication";
export {withAuthentication}
App
component so the child components like the Navigation
component can access all the logic that deals with the authenticated user. Make the following change in src/App/js
import { withAuthentication } from './components/Session';
...
export default withAuthentication(App);
src/Components/Navigation.js
...
import {
AppBar,
Box,
Button,
Link,
makeStyles,
Toolbar,
Typography,
} from '@material-ui/core';
import React, { useContext } from 'react';
import AuthUserContext from '../context/Session';
...
export default function Navigation() {
...
const {authUser} = useContext(AuthUserContext);
return(
...
<Typography variant="h6" className={classes.title}>
<Link
color="inherit"
href="#"
underline="none"
onClick={(e) => {
e.preventDefault();
history.push(ROUTES.LANDING);
}}
>
Expense Tracker
</Link>
</Typography>
{authUser ? (
<>
{authUser.name && (
<Box mr={3}>
<Typography variant="h6" color="inherit">
Hello, {authUser.name}
</Typography>
</Box>
)}
<Button color="inherit" onClick={() => history.push(ROUTES.HOME)}>
Home
</Button>
<Button color="inherit">Sign Out</Button>
</>
) : (
<>
<Button
color="inherit"
onClick={() => history.push(ROUTES.SIGN_UP)}
>
Sign Up
</Button>
<Button
color="inherit"
onClick={() => history.push(ROUTES.SIGN_IN)}
>
Sign In
</Button>
</>
)}
...
);
src/components/Navigation.jsx
...
import { AppwriteContext } from './Appwrite';
...
export default function Navigation() {
...
const {authUser, getCurrentUser} = useContext(AuthUserContext);
const appwrite = useContext(AppwriteContext);
const handleLogout = () => {
appwrite
.doLogout()
.then(() => {
getCurrentUser();
history.push(ROUTES.LANDING);
})
.catch((err) => console.log(err));
};
return (
...
<Button color="inherit" onClick={handleLogout}>
Sign Out
</Button>
...
);
PrivateRoute
component which will be used as a wrapper for routes that require authentication. The wrapper will render a passed component on the condition that the authUser
is present, otherwise, it will redirect to the login route.src/components/PrivateRoute/index.jsx
, and add the following codeimport React, { useEffect, useContext } from 'react';
import AuthUserContext from '../../context/Session';
import { Route, Redirect } from "react-router-dom";
import * as ROUTES from '../../constants/routes';
const PrivateRoute = ({ component: Component, ...rest }) => {
const { authUser } = useContext(AuthUserContext);
return (
<Route {...rest}
render={props => authUser ?
(<Component {...props} />) :
(<Redirect to={{ pathname: ROUTES.SIGN_IN, state: { from: props.location } }} />)}
/>
)
}
export default PrivateRoute;
state
prop in the Redirect
component. This will be used to redirect the user back to the page they were trying to access before they were authenticated.Home
route. To do that, we simply import PrivateRoute
in our App
component and change the home route from Route
to PrivateRoute
....
import PrivateRoute from './components/PrivateRoute';
function App() {
return (
<Router>
...
<PrivateRoute exact path={ROUTES.HOME} component={Home} />
...
</Router>
);
}
src/pages/Auth/SignIn.jsx
, make the following changes:import React, { useState, useContext, useEffect } from 'react';
...
import * as ROUTES from '../../constants/routes';
import { useHistory } from 'react-router-dom';
import AuthUserContext from '../../context/Session';
...
export default function SignIn() {
...
const {authUser, getCurrentUser} = useContext(AuthUserContext);
const history = useHistory();
useEffect(() => {
if(authUser) {
history.replace(ROUTES.HOME)
}
}, [authUser]);
const onSubmit = (e) => {
e.preventDefault();
if ( email === '' || password === '') {
alert('All fields are required');
return;
}
appwrite.doLogin(email, password).then(() => {
getCurrentUser();
const locationState = history.location.state;
let redirectTo = ROUTES.HOME;
if (locationState && locationState.from.pathname) {
redirectTo = locationState.from.pathname;
}
history.replace(redirectTo);
}).catch((error) => {
console.log('Error', error);
});
}
src/pages/Auth/SignUp.jsx
....
import * as ROUTES from '../../constants/routes';
import { useHistory } from 'react-router-dom';
...
export default function SignUp() {
...
const history = useHistory();
const onSubmit = (e) => {
e.preventDefault();
if (name === '' || email === '' || password === '') {
alert('All fields are required');
return;
}
appwrite
.doCreateAccount(email, password, name)
.then(() => {
history.replace(ROUTES.SIGN_IN);
})
.catch((error) => {
console.log('Error', error);
});
};
...