├── .prettierignore ├── src ├── types │ ├── todo │ │ ├── itodo.ts │ │ └── itodo-state.ts │ ├── forms │ │ ├── Validators.ts │ │ ├── IForm.ts │ │ ├── IFormField.ts │ │ ├── Form.ts │ │ └── FormField.ts │ ├── userbase │ │ ├── ierror.ts │ │ └── isignup-dto.ts │ ├── login │ │ ├── ilogin-form.ts │ │ └── ilogin-dto.ts │ ├── signup │ │ └── isignup-state.ts │ ├── auth │ │ ├── iauth-route-props.ts │ │ └── iauth-route-state.ts │ ├── forgot-password │ │ └── iforgot-password-form.ts │ └── profile │ │ ├── iupdate-account-info-dto.ts │ │ └── iprofile.ts ├── images │ └── gatsby-icon.png ├── styles │ ├── auth.scss │ ├── layout.scss │ ├── forms.scss │ └── site.scss ├── components │ ├── footer │ │ └── index.tsx │ ├── layout.tsx │ ├── auth │ │ ├── private-route │ │ │ └── index.tsx │ │ └── public-route │ │ │ └── index.tsx │ ├── header │ │ └── index.tsx │ ├── seo.tsx │ ├── forgot-password │ │ └── index.tsx │ ├── login │ │ └── index.tsx │ ├── todo │ │ └── index.tsx │ ├── signup │ │ └── index.tsx │ └── profile │ │ └── index.tsx └── pages │ ├── 404.tsx │ ├── index.tsx │ └── app.tsx ├── .prettierrc ├── gatsby-node.js ├── gatsby-browser.js ├── gatsby-ssr.js ├── tailwind.config.js ├── LICENSE ├── .gitignore ├── gatsby-config.js ├── README.md └── package.json /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | -------------------------------------------------------------------------------- /src/types/todo/itodo.ts: -------------------------------------------------------------------------------- 1 | export interface ITodo { 2 | name: string; 3 | completed: boolean; 4 | } -------------------------------------------------------------------------------- /src/types/forms/Validators.ts: -------------------------------------------------------------------------------- 1 | export enum Validators { 'required', 'email', 'matchFormField', 'none' }; 2 | -------------------------------------------------------------------------------- /src/images/gatsby-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jneterer/userbase-gatsby-starter/HEAD/src/images/gatsby-icon.png -------------------------------------------------------------------------------- /src/types/forms/IForm.ts: -------------------------------------------------------------------------------- 1 | export interface IForm { 2 | changed: boolean; 3 | valid: boolean; 4 | submitted: boolean; 5 | } -------------------------------------------------------------------------------- /src/types/userbase/ierror.ts: -------------------------------------------------------------------------------- 1 | export interface IError { 2 | name: string; 3 | message: string; 4 | status: number; 5 | } -------------------------------------------------------------------------------- /src/styles/auth.scss: -------------------------------------------------------------------------------- 1 | .auth-form { 2 | @apply mb-4; 3 | @screen sm { 4 | @apply bg-white shadow-lg rounded px-8 pt-6 pb-8; 5 | } 6 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "singleQuote": false, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /src/types/login/ilogin-form.ts: -------------------------------------------------------------------------------- 1 | import { Form } from "../forms/Form"; 2 | 3 | export interface ILoginState { 4 | loginForm: Form; 5 | loginError: string; 6 | } -------------------------------------------------------------------------------- /src/types/signup/isignup-state.ts: -------------------------------------------------------------------------------- 1 | import { Form } from "../forms/Form"; 2 | 3 | export interface ISignupState { 4 | signupForm: Form; 5 | signupError: string; 6 | } -------------------------------------------------------------------------------- /src/types/auth/iauth-route-props.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from "react" 2 | 3 | export interface IAuthRouteProps { 4 | path: string; 5 | component: ComponentClass; 6 | } -------------------------------------------------------------------------------- /gatsby-node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Node APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/node-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /gatsby-browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's Browser APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/browser-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /gatsby-ssr.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Implement Gatsby's SSR (Server Side Rendering) APIs in this file. 3 | * 4 | * See: https://www.gatsbyjs.org/docs/ssr-apis/ 5 | */ 6 | 7 | // You can delete this file if you're not using it 8 | -------------------------------------------------------------------------------- /src/types/forgot-password/iforgot-password-form.ts: -------------------------------------------------------------------------------- 1 | import { Form } from "../forms/Form"; 2 | 3 | export interface IForgotPasswordState { 4 | forgotPasswordForm: Form; 5 | emailSent: boolean; 6 | submissionError: string; 7 | } -------------------------------------------------------------------------------- /src/styles/layout.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font: georgia, serif; 3 | box-sizing: border-box; 4 | overflow-y: scroll; 5 | } 6 | body { 7 | margin: 0; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } -------------------------------------------------------------------------------- /src/types/login/ilogin-dto.ts: -------------------------------------------------------------------------------- 1 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 2 | 3 | export interface ILoginDto { 4 | username: string; 5 | password: string; 6 | rememberMe: userbase.RememberMeOption; 7 | } -------------------------------------------------------------------------------- /src/types/auth/iauth-route-state.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from "react" 2 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 3 | 4 | export interface IAuthRouteState { 5 | user: userbase.UserResult; 6 | isLoading: boolean; 7 | page: ComponentClass; 8 | } -------------------------------------------------------------------------------- /src/types/userbase/isignup-dto.ts: -------------------------------------------------------------------------------- 1 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 2 | 3 | export interface ISignupDto { 4 | username: string; 5 | password: string; 6 | email: string; 7 | profile?: userbase.UserProfile; 8 | rememberMe?: userbase.RememberMeOption 9 | } -------------------------------------------------------------------------------- /src/types/forms/IFormField.ts: -------------------------------------------------------------------------------- 1 | import { Validators } from "./Validators"; 2 | 3 | export interface IFormField { 4 | name: string; 5 | value: string; 6 | changed: boolean; 7 | touched: boolean; 8 | error: Validators; 9 | matchFormField: string; 10 | validators: Validators[]; 11 | valid: boolean; 12 | } -------------------------------------------------------------------------------- /src/types/profile/iupdate-account-info-dto.ts: -------------------------------------------------------------------------------- 1 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 2 | 3 | export interface IUpdateAccountInfoDto { 4 | username: string; 5 | currentPassword?: string; 6 | newPassword?: string; 7 | email?: string; 8 | profile?: userbase.UserProfile; 9 | } -------------------------------------------------------------------------------- /src/components/footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Footer = () => ( 4 | 9 | ) 10 | 11 | export default Footer 12 | -------------------------------------------------------------------------------- /src/types/todo/itodo-state.ts: -------------------------------------------------------------------------------- 1 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 2 | import { Form } from "../forms/Form"; 3 | 4 | export interface ITodoState { 5 | user: userbase.UserResult; 6 | todos: Item[]; 7 | generalError: string; 8 | addTodoForm: Form; 9 | addTodoFormError: string; 10 | } -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Components 4 | import Layout from "../components/layout"; 5 | import SEO from "../components/seo"; 6 | 7 | const NotFoundPage = () => ( 8 | 9 | 10 |

NOT FOUND

11 |

You just hit a route that doesn't exist... the sadness.

12 |
13 | ) 14 | 15 | export default NotFoundPage 16 | -------------------------------------------------------------------------------- /src/types/profile/iprofile.ts: -------------------------------------------------------------------------------- 1 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 2 | import { Form } from "../forms/Form"; 3 | 4 | export interface IProfileState { 5 | user: userbase.UserResult 6 | accountInfoForm: Form; 7 | accountInfoFormSuccess: boolean; 8 | accountInfoFormError: string; 9 | changePasswordForm: Form; 10 | changePasswordFormSuccess: boolean; 11 | changePasswordFormError: string; 12 | deleteAccountRequested: boolean; 13 | deleteAccountSuccess: boolean; 14 | deleteAccountError: string; 15 | } -------------------------------------------------------------------------------- /src/styles/forms.scss: -------------------------------------------------------------------------------- 1 | // Default input style 2 | input:not([type="checkbox"]) { 3 | @apply shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight transition duration-200 ease-linear; 4 | @apply relative z-20; 5 | &:focus { 6 | @apply outline-none shadow-outline; 7 | } 8 | } 9 | 10 | // Default label style 11 | label { 12 | @apply block text-gray-700 text-sm font-bold mb-2; 13 | } 14 | 15 | // Default form input error style 16 | .error-msg { 17 | @apply text-red-500 text-xs italic mt-1; 18 | @apply relative z-10; 19 | top: -1.5rem; 20 | height: 0px; 21 | @apply transition-all duration-200; 22 | &.show { 23 | top: 0; 24 | height: 1rem; 25 | @apply transition-all duration-200; 26 | } 27 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | inset: { 4 | '20': '5rem', 5 | }, 6 | boxShadow: { 7 | default: '0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06)', 8 | md: ' 0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06)', 9 | lg: ' 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05)', 10 | xl: ' 0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04)', 11 | ['2xl']: '0 25px 50px -12px rgba(0, 0, 0, .25)', 12 | ['3xl:']: '0 35px 60px -15px rgba(0, 0, 0, .3)', 13 | inner: 'inset 0 2px 4px 0 rgba(0,0,0,0.06)', 14 | outline: '0 0 0 3px rgba(86, 60, 154,0.5)', 15 | focus: '0 0 0 3px rgba(86, 60, 154,0.5)', 16 | } 17 | }, 18 | variants: {}, 19 | plugins: [] 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // Components 4 | import Layout from "../components/layout"; 5 | import SEO from "../components/seo"; 6 | 7 | // Styles 8 | import '../styles/site.scss'; 9 | import '../styles/layout.scss'; 10 | 11 | const IndexPage = () => ( 12 | 13 | 14 |

15 | Another Todo 16 |

17 |

18 | Built using the exciting new serverless JavaScript 19 | {` `}SDK Userbase. 20 |

21 |

You can read how to set up this Userbase Gatsby Starter here: README.

22 |
23 | ) 24 | 25 | export default IndexPage 26 | -------------------------------------------------------------------------------- /src/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { useStaticQuery, graphql } from "gatsby"; 4 | 5 | // Components 6 | import Header from "./header"; 7 | import Footer from "./footer"; 8 | 9 | const Layout = ({ children, ...rest }) => { 10 | const data = useStaticQuery(graphql` 11 | query SiteTitleQuery { 12 | site { 13 | siteMetadata { 14 | title 15 | } 16 | } 17 | } 18 | `) 19 | return ( 20 | <> 21 |
22 |
23 |
{children}
24 |
25 |
26 | 27 | ) 28 | } 29 | 30 | Layout.propTypes = { 31 | children: PropTypes.node.isRequired, 32 | user: PropTypes.object, 33 | } 34 | 35 | Layout.defaultProps = { 36 | user: null 37 | } 38 | 39 | export default Layout 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 gatsbyjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # dotenv environment variable files 55 | .env* 56 | 57 | # gatsby files 58 | .cache/ 59 | public 60 | 61 | # Mac files 62 | .DS_Store 63 | 64 | # Yarn 65 | yarn-error.log 66 | .pnp/ 67 | .pnp.js 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | -------------------------------------------------------------------------------- /src/pages/app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Router, RouteComponentProps, Redirect } from "@reach/router"; 3 | 4 | // Auth 5 | import PrivateRoute from "../components/auth/private-route"; 6 | import PublicRoute from "../components/auth/public-route"; 7 | 8 | // Components 9 | import ForgotPassword from "../components/forgot-password"; 10 | import Login from "../components/login"; 11 | import Profile from "../components/profile"; 12 | import Signup from "../components/signup"; 13 | import Todo from "../components/todo"; 14 | 15 | // Pages 16 | import IndexPage from "." 17 | 18 | class App extends React.Component<{}> { 19 | render() { 20 | return ( 21 | 22 | } /> 23 | 24 | 25 | 26 | 27 | 28 | 29 | } /> 30 | 31 | ) 32 | } 33 | } 34 | 35 | export default App 36 | 37 | const RouterPage = ( 38 | props: { pageComponent: JSX.Element } & RouteComponentProps 39 | ) => props.pageComponent; -------------------------------------------------------------------------------- /gatsby-config.js: -------------------------------------------------------------------------------- 1 | require("dotenv").config({ 2 | path: `.env`, 3 | }) 4 | module.exports = { 5 | siteMetadata: { 6 | title: `AnotherTodo`, 7 | description: `Another Todo web app that uses Userbase.`, 8 | author: `@gatsbyjs`, 9 | }, 10 | plugins: [ 11 | `gatsby-plugin-react-helmet`, 12 | { 13 | resolve: `gatsby-source-filesystem`, 14 | options: { 15 | name: `images`, 16 | path: `${__dirname}/src/images`, 17 | }, 18 | }, 19 | `gatsby-transformer-sharp`, 20 | `gatsby-plugin-sharp`, 21 | { 22 | resolve: `gatsby-plugin-manifest`, 23 | options: { 24 | name: `gatsby-starter-default`, 25 | short_name: `starter`, 26 | start_url: `/`, 27 | background_color: `#663399`, 28 | theme_color: `#663399`, 29 | display: `minimal-ui`, 30 | icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site. 31 | }, 32 | }, 33 | `gatsby-plugin-typescript`, 34 | { 35 | resolve: `gatsby-plugin-sass`, 36 | options: { 37 | postCssPlugins: [ 38 | require("tailwindcss"), 39 | require("./tailwind.config.js") 40 | ] 41 | } 42 | }, 43 | { 44 | resolve: `gatsby-plugin-create-client-paths`, 45 | options: { prefixes: [`/app/*`] }, 46 | } 47 | // this (optional) plugin enables Progressive Web App + Offline functionality 48 | // To learn more, visit: https://gatsby.dev/offline 49 | // `gatsby-plugin-offline`, 50 | ], 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatsby Starter - Userbase, TailwindCSS, Sass, & Typescript [![Netlify Status](https://api.netlify.com/api/v1/badges/1a5f926a-5ec4-4180-a217-d8b99b9ecd48/deploy-status)](https://app.netlify.com/sites/userbase-gatsby-starter/deploys) 2 | 3 | This [Gatsby](https://www.gatsbyjs.org/) starter project is for [Userbase](https://userbase.com/) web apps. It ships with all user and data APIs! The app uses [TailwindCSS](https://tailwindcss.com/) for the styling framework, the preprocessor [Sass](https://sass-lang.com/), and [Typescript](https://www.typescriptlang.org/). 4 | 5 | ## Demo Site 6 | 7 | The demo site for this project can be found [here](https://userbase-gatsby-starter.jacobneterer.com/). 8 | 9 | ## Getting Started 10 | 11 | Assuming that you already have gatsby installed, install the Userbase Gatsby Starter: 12 | ``` 13 | gatsby new userbase-gatsby https://github.com/jneterer/userbase-gatsby-starter 14 | ``` 15 | 16 | ### Running in development 17 | 18 | `gatsby develop` 19 | 20 | ### Set up basic environment variables 21 | 22 | Create a `.env` file in the root of your project. Add a new environment variable `GATSBY_REACT_APP_USERBASE_APP_ID={YOUR_USERBASE_APP_ID}` replacing `{YOUR_USERBASE_APP_ID}` with the app id listed in your userbase account. 23 | 24 | Next, add this at the beginning of your `gatsby-config.js` file to allow for environment variable support: 25 | 26 | ``` 27 | require("dotenv").config({ 28 | path: `.env`, 29 | }) 30 | ``` 31 | ## Thanks! 32 | Thank you for trying out my Userbase Gatsby Starter project! You can find me here: [My Site](https://jacobneterer.com), [Twitter](https://twitter.com/jacobneterer), and [LinkedIn](https://www.linkedin.com/in/jacobneterer). 33 | -------------------------------------------------------------------------------- /src/components/auth/private-route/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from "@reach/router"; 3 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 4 | 5 | // Components 6 | import Layout from "../../layout"; 7 | 8 | // Types 9 | import { IAuthRouteProps } from "../../../types/auth/iauth-route-props"; 10 | import { IAuthRouteState } from "../../../types/auth/iauth-route-state"; 11 | 12 | class PrivateRoute extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | user: null, 17 | isLoading: true, 18 | page: this.props.component 19 | } 20 | } 21 | 22 | componentDidMount() { 23 | // Check the user's session. 24 | userbase.init({ appId: process.env.GATSBY_REACT_APP_USERBASE_APP_ID as string }) 25 | .then(session => { 26 | this.setState((state) => { 27 | return { 28 | ...state, 29 | isLoading: false, 30 | user: session.user ? session.user : null 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | render() { 37 | const Page = this.state.page; 38 | // If the page is loading, indicate we are still performing our session check. 39 | if (this.state.isLoading) { 40 | return

LOADING...

41 | } else if (this.state.user) { 42 | // If we have completed our session check and the user is logged in, send them to the requested page. 43 | return 44 | } 45 | // Otherwise, redirect to the login page. 46 | return 47 | } 48 | 49 | } 50 | 51 | export default PrivateRoute -------------------------------------------------------------------------------- /src/components/auth/public-route/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Redirect } from "@reach/router"; 3 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 4 | 5 | // Components 6 | import Layout from "../../layout"; 7 | 8 | // Types 9 | import { IAuthRouteProps } from "../../../types/auth/iauth-route-props"; 10 | import { IAuthRouteState } from "../../../types/auth/iauth-route-state"; 11 | 12 | class PublicRoute extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | this.state = { 16 | user: null, 17 | isLoading: true, 18 | page: this.props.component 19 | } 20 | } 21 | 22 | componentDidMount() { 23 | // Check the user's session and set the state. 24 | userbase.init({ appId: process.env.GATSBY_REACT_APP_USERBASE_APP_ID as string }) 25 | .then(session => { 26 | this.setState((state) => { 27 | return { 28 | ...state, 29 | isLoading: false, 30 | user: session.user ? session.user : null 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | render() { 37 | const Page = this.state.page; 38 | // If the page is loading, indicate we are still performing our session check. 39 | if (this.state.isLoading) { 40 | return

LOADING...

41 | } else if (this.state.user) { 42 | // If we have completed our session check and the user is already log in, send them to the todo page. 43 | return 44 | } 45 | // Otherwise, send them to the page they've requested. 46 | return 47 | } 48 | 49 | } 50 | 51 | export default PublicRoute -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gatsby-starter-default", 3 | "private": true, 4 | "description": "A simple starter to get up and developing quickly with Gatsby", 5 | "version": "0.1.0", 6 | "author": "Kyle Mathews ", 7 | "dependencies": { 8 | "@reach/router": "^1.3.1", 9 | "@types/reach__router": "^1.2.6", 10 | "gatsby": "^2.19.7", 11 | "gatsby-image": "^2.2.39", 12 | "gatsby-plugin-create-client-paths": "^2.1.22", 13 | "gatsby-plugin-manifest": "^2.2.39", 14 | "gatsby-plugin-offline": "^3.0.32", 15 | "gatsby-plugin-react-helmet": "^3.1.21", 16 | "gatsby-plugin-sass": "^2.1.27", 17 | "gatsby-plugin-sharp": "^2.4.3", 18 | "gatsby-plugin-typescript": "^2.1.26", 19 | "gatsby-source-filesystem": "^2.1.46", 20 | "gatsby-transformer-sharp": "^2.3.13", 21 | "node-sass": "^4.13.1", 22 | "prop-types": "^15.7.2", 23 | "react": "^16.12.0", 24 | "react-dom": "^16.12.0", 25 | "react-helmet": "^5.2.1", 26 | "userbase-js": "^1.1.0" 27 | }, 28 | "devDependencies": { 29 | "prettier": "^1.19.1", 30 | "tailwindcss": "^1.2.0" 31 | }, 32 | "keywords": [ 33 | "gatsby" 34 | ], 35 | "license": "MIT", 36 | "scripts": { 37 | "build": "gatsby build", 38 | "develop": "gatsby develop", 39 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"", 40 | "start": "npm run develop", 41 | "serve": "gatsby serve", 42 | "clean": "gatsby clean", 43 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/gatsbyjs/gatsby-starter-default" 48 | }, 49 | "bugs": { 50 | "url": "https://github.com/gatsbyjs/gatsby/issues" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { Link } from "gatsby"; 4 | import { navigate } from "@reach/router"; 5 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 6 | 7 | const Header = ({ siteTitle, user }) => ( 8 |
9 |

10 | 17 | {siteTitle} 18 | 19 |

20 | { 21 | user ? 22 | : 37 | 38 | 41 | 42 | } 43 |
44 | ) 45 | 46 | const signOut = () => { 47 | userbase.signOut() 48 | .then(() => { 49 | navigate('/app/login') 50 | }) 51 | .catch((e) => console.error(e)) 52 | } 53 | 54 | Header.propTypes = { 55 | siteTitle: PropTypes.string, 56 | user: PropTypes.object, 57 | } 58 | 59 | Header.defaultProps = { 60 | siteTitle: ``, 61 | user: null 62 | } 63 | 64 | export default Header 65 | -------------------------------------------------------------------------------- /src/components/seo.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import Helmet from "react-helmet"; 4 | import { useStaticQuery, graphql } from "gatsby"; 5 | 6 | function SEO({ description, lang, meta, title }) { 7 | const { site } = useStaticQuery( 8 | graphql` 9 | query { 10 | site { 11 | siteMetadata { 12 | title 13 | description 14 | author 15 | } 16 | } 17 | } 18 | ` 19 | ) 20 | 21 | const metaDescription = description || site.siteMetadata.description 22 | 23 | return ( 24 | 65 | ) 66 | } 67 | 68 | SEO.defaultProps = { 69 | lang: `en`, 70 | meta: [], 71 | description: ``, 72 | } 73 | 74 | SEO.propTypes = { 75 | description: PropTypes.string, 76 | lang: PropTypes.string, 77 | meta: PropTypes.arrayOf(PropTypes.object), 78 | title: PropTypes.string.isRequired, 79 | } 80 | 81 | export default SEO 82 | -------------------------------------------------------------------------------- /src/styles/site.scss: -------------------------------------------------------------------------------- 1 | // Import tailwind styles 2 | @import "tailwindcss/base"; 3 | 4 | @import "tailwindcss/components"; 5 | 6 | @import "tailwindcss/utilities"; 7 | 8 | // Import default styles for different aspects of the site 9 | @import './forms.scss'; 10 | @import './auth.scss'; 11 | 12 | // Default anchor style 13 | a { 14 | &.link { 15 | @apply font-black; 16 | color: theme('colors.purple.800') !important; 17 | text-decoration: underline !important; 18 | transition: color .25s ease; 19 | &:hover { 20 | color: theme('colors.black') !important; 21 | transition: color .25s ease; 22 | } 23 | } 24 | &:active, &:hover { 25 | outline-width: 0; 26 | } 27 | } 28 | 29 | @mixin btn-style($bg-color, $hover-bg-color, $text-color, $hover-text-color: false) { 30 | background-color: theme($bg-color); 31 | color: theme($text-color); 32 | @screen sm { 33 | @apply py-2 px-10; 34 | } 35 | @apply py-2 px-8 font-semibold rounded; 36 | @if $hover-text-color { 37 | transition: background-color .25s, color .25s ease; 38 | } @else { 39 | transition: background-color .25s ease; 40 | } 41 | &:not(:disabled):hover { 42 | background-color: theme($hover-bg-color); 43 | @if $hover-text-color { 44 | color: theme($hover-text-color); 45 | transition: background-color .25s, color .25s ease; 46 | } @else { 47 | transition: background-color .25s ease; 48 | } 49 | } 50 | &:active, &:focus { 51 | outline: none; 52 | } 53 | } 54 | 55 | .btn { 56 | &-primary { 57 | @include btn-style('colors.purple.800', 'colors.purple.300', 'colors.white', 'colors.black'); 58 | } 59 | &-secondary { 60 | @include btn-style('colors.white', 'colors.black', 'colors.black', 'colors.white'); 61 | } 62 | &-accent { 63 | @include btn-style('colors.purple.800', 'colors.black', 'colors.white'); 64 | border: 1px solid white; 65 | } 66 | &-error { 67 | @include btn-style('colors.red.600', 'colors.red.700', 'colors.white'); 68 | } 69 | &-icon { 70 | @apply rounded-full h-6 w-6 flex items-center justify-center; 71 | } 72 | } 73 | 74 | button:disabled { 75 | opacity: .5; 76 | @apply transition-all duration-200; 77 | &:hover { 78 | cursor: default; 79 | } 80 | } 81 | 82 | .error { 83 | color: theme('colors.red.600'); 84 | } 85 | .success { 86 | color: theme('colors.green.600'); 87 | } 88 | -------------------------------------------------------------------------------- /src/types/forms/Form.ts: -------------------------------------------------------------------------------- 1 | import { FormField } from "./FormField"; 2 | import { IForm } from "./IForm"; 3 | import { Validators } from "./Validators"; 4 | 5 | export class Form { 6 | private formFields: object = {}; 7 | private properties: IForm = { 8 | changed: false, 9 | valid: true, 10 | submitted: false 11 | }; 12 | 13 | constructor(formFields: FormField[]) { 14 | formFields.forEach((formField: FormField) => this.formFields[formField.name] = formField ); 15 | } 16 | 17 | /** 18 | * Gets and returns the changed property of the form. 19 | * @returns {boolean} Returns true if any of the form fields have changed. 20 | */ 21 | get changed(): boolean { 22 | return this.properties.changed; 23 | } 24 | 25 | /** 26 | * Gets and returns the valid property of the form. 27 | * @returns {boolean} Returns true if the form is valid and false if the form is invalid. 28 | */ 29 | get valid(): boolean { 30 | return this.properties.valid; 31 | } 32 | 33 | /** 34 | * Gets and returns the submitted property of the form. 35 | * @returns {boolean} Returns true if the form has been submitted. 36 | */ 37 | get submitted(): boolean { 38 | return this.properties.submitted; 39 | } 40 | 41 | /** 42 | * Sets the submitted property on the form given the new value. 43 | * Also checks all the form fields' validity and updates accordingly. 44 | * @param {boolean} value The value we wish to set the submitted property on the form. 45 | */ 46 | setSubmitted(value: boolean): void { 47 | this.properties.submitted = value; 48 | Object.keys(this.formFields).forEach((formFieldName: string) => { 49 | this.checkFormFieldValidity(formFieldName); 50 | }); 51 | this.checkFormIsValid(); 52 | } 53 | 54 | /** 55 | * Gets a form field from the form given the form field name. 56 | * @param {string} formFieldName The name of the form field we wish to retrieve. 57 | * @returns {FormField} 58 | */ 59 | getFormField(formFieldName: string): FormField { 60 | if (this.formFields.hasOwnProperty(formFieldName)) { 61 | return this.formFields[formFieldName]; 62 | } 63 | throw TypeError(`No form field exists on the form named ${formFieldName}.`); 64 | } 65 | 66 | /** 67 | * Sets a new value for a form field given the form field name and new value. 68 | * Also checks the form field's validity and updates accordingly. 69 | * @param {string} formFieldName The name of the form field we wish to set a new value to. 70 | * @param {string} value The value we wish to set the form field's value property to. 71 | */ 72 | setFormFieldValue(formFieldName: string, value: string): void { 73 | this.properties.changed = true; 74 | this.getFormField(formFieldName).setValue(value); 75 | this.checkFormFieldValidity(formFieldName); 76 | this.checkFormIsValid(); 77 | } 78 | 79 | /** 80 | * Sets a new touched value for a form field given the form field name and new value. 81 | * Also checks the form field's and form's validity and updates accordingly. 82 | * @param {string} formFieldName The name of the form field we wish to set a new touched value to. 83 | * @param {boolean} value The value we wish to set the form field's touched property to. 84 | */ 85 | setFormFieldTouched(formFieldName: string, value: boolean): void { 86 | this.getFormField(formFieldName).setTouched(value); 87 | this.checkFormFieldValidity(formFieldName); 88 | this.checkFormIsValid(); 89 | } 90 | 91 | /** 92 | * Checks the validity of a form field given the form field name. 93 | * @param {string} formFieldName The name of the form field we wish to check its validity. 94 | */ 95 | private checkFormFieldValidity(formFieldName: string): void { 96 | let formField: FormField = this.getFormField(formFieldName); 97 | const includesRequired: boolean = formField.validators.includes(Validators.required); 98 | const includesMatchForField: boolean = formField.validators.includes(Validators.matchFormField); 99 | if (includesRequired && includesMatchForField) { 100 | formField.checkFormFieldValidity(this.submitted, this.getFormField(formField.matchFormFieldProperty).value); 101 | } else if (includesRequired) { 102 | formField.checkFormFieldValidity(this.submitted); 103 | } else if (includesMatchForField) { 104 | formField.checkFormFieldValidity(null, this.getFormField(formField.matchFormFieldProperty).value); 105 | } else { 106 | formField.checkFormFieldValidity(); 107 | } 108 | } 109 | 110 | /** 111 | * Filters the form fields for any that may be invalid. Sets the valid property accordingly. 112 | */ 113 | private checkFormIsValid() { 114 | this.properties.valid = Object.keys(this.formFields).filter((formFieldName: string) => { 115 | return !this.getFormField(formFieldName).valid 116 | }).length === 0; 117 | } 118 | 119 | /** 120 | * Resets the form back to its original state. 121 | */ 122 | resetForm(): void { 123 | Object.keys(this.formFields).forEach((formFieldName: string) => { 124 | this.getFormField(formFieldName).resetFormField(); 125 | }); 126 | this.properties = { 127 | changed: false, 128 | valid: true, 129 | submitted: false 130 | }; 131 | } 132 | 133 | } -------------------------------------------------------------------------------- /src/components/forgot-password/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, FocusEvent } from "react"; 2 | import { Link } from "gatsby"; 3 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 4 | 5 | // Components 6 | import Layout from "../layout"; 7 | import SEO from "../../components/seo"; 8 | 9 | // Types 10 | import { Form } from "../../types/forms/Form"; 11 | import { FormField } from "../../types/forms/FormField"; 12 | import { IError } from "../../types/userbase/IError"; 13 | import { IForgotPasswordState } from "../../types/forgot-password/iforgot-password-form"; 14 | import { Validators } from "../../types/forms/Validators"; 15 | 16 | class ForgotPassword extends React.Component<{}, IForgotPasswordState> { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | forgotPasswordForm: new Form([ 21 | new FormField('username', '', [Validators.required]) 22 | ]), 23 | emailSent: false, 24 | submissionError: null 25 | }; 26 | this.handleInputChange = this.handleInputChange.bind(this); 27 | this.handleBlurEvent = this.handleBlurEvent.bind(this); 28 | this.handleSubmit = this.handleSubmit.bind(this); 29 | } 30 | 31 | /** 32 | * Sets the form value in the state. 33 | * @param {FormEvent} event 34 | */ 35 | handleInputChange(event: FormEvent) { 36 | event.persist(); 37 | const formFieldName: string = event.currentTarget.id; 38 | const newFormFieldValue: string = event.currentTarget.value; 39 | let forgotPasswordForm: Form = this.state.forgotPasswordForm; 40 | forgotPasswordForm.setFormFieldValue(formFieldName, newFormFieldValue); 41 | this.setState((state: IForgotPasswordState) => { 42 | return { 43 | ...state, 44 | forgotPasswordForm: forgotPasswordForm 45 | }; 46 | }); 47 | } 48 | 49 | /** 50 | * Sets the touched attribute for a form field. 51 | * @param {FocusEvent} event 52 | */ 53 | handleBlurEvent(event: FocusEvent) { 54 | event.persist(); 55 | const formFieldName: string = event.target.id; 56 | let forgotPasswordForm: Form = this.state.forgotPasswordForm; 57 | forgotPasswordForm.setFormFieldTouched(formFieldName, true); 58 | this.setState((state: IForgotPasswordState) => { 59 | return { 60 | ...state, 61 | forgotPasswordForm: forgotPasswordForm 62 | }; 63 | }); 64 | } 65 | 66 | /** 67 | * Submits a request for password recovery. 68 | * @param {FormEvent} event 69 | */ 70 | handleSubmit(event: FormEvent) { 71 | event.preventDefault(); 72 | let forgotPasswordForm: Form = this.state.forgotPasswordForm; 73 | forgotPasswordForm.setSubmitted(true); 74 | this.setState((state: IForgotPasswordState) => { 75 | return { 76 | ...state, 77 | forgotPasswordForm: forgotPasswordForm 78 | }; 79 | }); 80 | if (this.state.forgotPasswordForm.valid) { 81 | userbase.forgotPassword({ 82 | username: this.state.forgotPasswordForm.getFormField('username').value 83 | }) 84 | .then((user) => { 85 | this.setState((state) => { 86 | return { 87 | ...state, 88 | emailSent: true, 89 | submissionError: null 90 | }; 91 | }); 92 | }) 93 | .catch((error: IError) => { 94 | this.setState((state) => { 95 | return { 96 | ...state, 97 | emailSent: false, 98 | submissionError: error.message 99 | }; 100 | }); 101 | }); 102 | } 103 | } 104 | 105 | render() { 106 | return ( 107 | 108 | 109 |
110 |
111 |
112 |

Forgot Password

113 |
114 |
115 | 118 | 119 |

120 | This field is required. 121 |

122 |
123 |
124 | 127 |
128 | { 129 | this.state.emailSent && 130 |

Your forgot password request was submitted, please check your email.

131 | } 132 | { 133 | this.state.submissionError && 134 |

{this.state.submissionError}

135 | } 136 |
137 |
138 | 139 | Back to log in 140 | 141 |
142 |
143 |
144 |
145 | ); 146 | } 147 | } 148 | 149 | export default ForgotPassword; -------------------------------------------------------------------------------- /src/components/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, FocusEvent } from "react"; 2 | import { Link, navigate } from "gatsby"; 3 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 4 | 5 | // Components 6 | import Layout from "../layout"; 7 | import SEO from "../../components/seo"; 8 | 9 | // Types 10 | import { Form } from "../../types/forms/Form"; 11 | import { FormField } from "../../types/forms/FormField"; 12 | import { IError } from "../../types/userbase/IError"; 13 | import { ILoginDto } from "../../types/login/ilogin-dto"; 14 | import { ILoginState } from "../../types/login/ilogin-form"; 15 | import { Validators } from "../../types/forms/Validators"; 16 | 17 | class Login extends React.Component<{}, ILoginState> { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | loginForm: new Form([ 22 | new FormField('username', '', [Validators.required]), 23 | new FormField('password', '', [Validators.required]) 24 | ]), 25 | loginError: null 26 | }; 27 | this.handleInputChange = this.handleInputChange.bind(this); 28 | this.handleBlurEvent = this.handleBlurEvent.bind(this); 29 | this.handleSubmit = this.handleSubmit.bind(this); 30 | } 31 | 32 | /** 33 | * Sets the form value in the state. 34 | * @param {FormEvent} event 35 | */ 36 | handleInputChange(event: FormEvent) { 37 | event.persist(); 38 | const formFieldName: string = event.currentTarget.id; 39 | const newFormFieldValue: string = event.currentTarget.value; 40 | let loginForm: Form = this.state.loginForm; 41 | loginForm.setFormFieldValue(formFieldName, newFormFieldValue); 42 | this.setState((state: ILoginState) => { 43 | return { 44 | ...state, 45 | loginForm: loginForm 46 | }; 47 | }); 48 | } 49 | 50 | /** 51 | * Sets the touched attribute for a form field. 52 | * @param {FocusEvent} event 53 | */ 54 | handleBlurEvent(event: FocusEvent) { 55 | event.persist(); 56 | const formFieldName: string = event.target.id; 57 | let loginForm: Form = this.state.loginForm; 58 | loginForm.setFormFieldTouched(formFieldName, true); 59 | this.setState((state: ILoginState) => { 60 | return { 61 | ...state, 62 | loginForm: loginForm 63 | }; 64 | }); 65 | } 66 | 67 | /** 68 | * Logs the user into the web app. 69 | * @param {FormEvent} event 70 | */ 71 | handleSubmit(event: FormEvent) { 72 | event.preventDefault(); 73 | let loginForm: Form = this.state.loginForm; 74 | loginForm.setSubmitted(true); 75 | this.setState({ loginForm: loginForm }); 76 | if (this.state.loginForm.valid) { 77 | const loginDto: ILoginDto = { 78 | username: this.state.loginForm.getFormField('username').value, 79 | password: this.state.loginForm.getFormField('password').value, 80 | rememberMe: 'local' 81 | }; 82 | userbase.signIn(loginDto) 83 | .then((user: userbase.UserResult) => { 84 | navigate(user['usedTempPassword'] ? '/app/profile/changePasswordNeeded' : '/app/todo'); 85 | }) 86 | .catch((error: IError) => { 87 | this.setState((state) => { 88 | return { 89 | ...state, 90 | loginError: error.message 91 | } 92 | }); 93 | }); 94 | } 95 | } 96 | 97 | render() { 98 | return ( 99 | 100 | 101 |
102 |
103 |
104 |

Log in to your account

105 |
106 |
107 | 110 | 111 |

112 | This field is required. 113 |

114 |
115 |
116 | 119 | 120 |

121 | This field is required. 122 |

123 |
124 |
125 | 126 | Forgot password? 127 | 128 |
129 |
130 | 133 |
134 | { 135 | this.state.loginError && 136 |

{this.state.loginError}

137 | } 138 |
139 |
140 | 141 | Sign up for an account 142 | 143 |
144 |
145 |
146 |
147 | ); 148 | } 149 | } 150 | 151 | export default Login; -------------------------------------------------------------------------------- /src/types/forms/FormField.ts: -------------------------------------------------------------------------------- 1 | import { IFormField } from './IFormField'; 2 | import { Validators } from "./Validators"; 3 | 4 | export class FormField { 5 | private properties: IFormField = { 6 | name: '', 7 | value: '', 8 | changed: false, 9 | touched: false, 10 | error: Validators.none, 11 | matchFormField: null, 12 | validators: [Validators.none], 13 | valid: true 14 | }; 15 | 16 | constructor(name: string, value: string, validators: Validators[] = [Validators.none], matchFormField: string = null, changed: boolean = false, touched: boolean = false, error: Validators = Validators.none, valid: boolean = true) { 17 | this.properties.name = name; 18 | this.properties.value = value; 19 | this.properties.changed = changed; 20 | this.properties.touched = touched; 21 | this.properties.error = error; 22 | this.properties.matchFormField = matchFormField; 23 | this.properties.validators = validators; 24 | this.properties.valid = valid; 25 | } 26 | 27 | /** 28 | * Gets the name property from the form field. 29 | * @returns {string} 30 | */ 31 | get name(): string { 32 | return this.properties.name; 33 | } 34 | 35 | /** 36 | * Gets the value property from the form field. 37 | * @returns {string} 38 | */ 39 | get value(): string { 40 | return this.properties.value; 41 | } 42 | 43 | /** 44 | * Gets the error property from the form field. 45 | * @returns {Validators} 46 | */ 47 | get error(): Validators { 48 | return this.properties.error; 49 | } 50 | 51 | /** 52 | * Gets the matchFormField property from the form field. 53 | * @returns {string} 54 | */ 55 | get matchFormFieldProperty(): string { 56 | return this.properties.matchFormField; 57 | } 58 | 59 | /** 60 | * Get the validators property from the form field. 61 | * @returns {Validators[]} 62 | */ 63 | get validators(): Validators[] { 64 | return this.properties.validators; 65 | } 66 | 67 | /** 68 | * Gets and returns the valid property of the form field. 69 | * @returns {boolean} Returns true if the form field is valid and false if the form field is invalid. 70 | */ 71 | get valid(): boolean { 72 | return this.properties.valid; 73 | } 74 | 75 | /** 76 | * Updates the value of the form field and also the changed property since the form field has changed. 77 | * @param {string} value The new value of the form field. 78 | */ 79 | public setValue(value: string): void { 80 | this.updateFormFieldProperty('value', value); 81 | this.updateFormFieldProperty('changed', true); 82 | } 83 | 84 | /** 85 | * Updates the touched property of the form field. 86 | * @param value The new value for the touched property of the form field. 87 | */ 88 | public setTouched(value: boolean): void { 89 | this.updateFormFieldProperty('touched', value); 90 | } 91 | 92 | /** 93 | * Updates a property on the form field with a given value. 94 | * @param {keyof IFormFIeld} property The property name we would like to update on the form field 95 | * @param value The value we would like to update for the property on the form field. 96 | */ 97 | private updateFormFieldProperty(property: keyof IFormField, value: (string | boolean )): void { 98 | this.properties = Object.assign({}, { 99 | ...this.properties, 100 | [property]: value 101 | }); 102 | } 103 | 104 | /** 105 | * Checks the validity of the form field. 106 | * @param formSubmitted Whether the form has been submitted or not. 107 | * @param matchFormFieldValue The value of the form this form field must match, if that is a required validator for this form field. 108 | */ 109 | checkFormFieldValidity(formSubmitted?: boolean, matchFormFieldValue?: string): void { 110 | this.properties.validators.some((validator: Validators) => { 111 | if (validator === Validators.none) { 112 | this.setFormFieldValidity(true, Validators.none); 113 | return true; 114 | } else if (validator === Validators.required) { 115 | const requiredValid: boolean = this.checkRequiredValid(formSubmitted); 116 | if (!requiredValid) { 117 | this.setFormFieldValidity(false, Validators.required); 118 | return true; 119 | } else { 120 | this.setFormFieldValidity(true, Validators.none); 121 | } 122 | } else if (validator === Validators.email) { 123 | const emailValid: boolean = this.checkEmailValid(); 124 | if (!emailValid) { 125 | this.setFormFieldValidity(false, Validators.email); 126 | return true; 127 | } else { 128 | this.setFormFieldValidity(true, Validators.none); 129 | } 130 | } 131 | else if (validator === Validators.matchFormField) { 132 | const matchValid: boolean = this.checkMatchValid(matchFormFieldValue); 133 | if (!matchValid) { 134 | this.setFormFieldValidity(false, Validators.matchFormField); 135 | return true; 136 | } else { 137 | this.setFormFieldValidity(true, Validators.none); 138 | } 139 | } 140 | this.properties.valid = true; 141 | return false; 142 | }); 143 | } 144 | 145 | /** 146 | * Checks if a required form field is valid. 147 | * @param {boolean} formSubmitted Whether the form has been submitted or not. 148 | * @returns {boolean} Returns true if the form field's required validator is valid. 149 | */ 150 | private checkRequiredValid(formSubmitted: boolean): boolean { 151 | return !formSubmitted || (formSubmitted && this.properties.value !== ''); 152 | } 153 | 154 | /** 155 | * Checks if the form field of type email is valid. 156 | * @returns {boolean} Returns true if the form field's email validator is valid. 157 | */ 158 | private checkEmailValid(): boolean { 159 | return (!this.properties.touched && !this.properties.changed) || 160 | (this.properties.touched && !this.properties.changed) || 161 | (!this.properties.touched && this.properties.changed && !this.properties.value.match(/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/) !== null) || 162 | (this.properties.touched && this.properties.changed && this.properties.value.match(/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/) !== null); 163 | } 164 | 165 | /** 166 | * Checks if the form field matches the form field it should equal. 167 | * @param {string} matchFormFieldValue The value of the form field that this form field must match. 168 | * @returns {boolean} Returns true if the form field matches the form field it should equal. 169 | */ 170 | private checkMatchValid(matchFormFieldValue: string): boolean { 171 | return !(this.properties.touched && this.properties.changed && this.properties.value !== matchFormFieldValue); 172 | } 173 | 174 | /** 175 | * 176 | * @param {boolean} valid The new value for the valid property on the form field. 177 | * @param {Validators} error The new value for the error property on the form field. 178 | */ 179 | private setFormFieldValidity(valid: boolean, error: Validators): void { 180 | this.properties.valid = valid; 181 | this.properties.error = error; 182 | } 183 | 184 | /** 185 | * Resets the form field back to its original state. This will overwrite any initial default 186 | * value for the form field but persist the validators and name. 187 | */ 188 | resetFormField(): void { 189 | this.properties = { 190 | ...this.properties, 191 | value: '', 192 | changed: false, 193 | touched: false, 194 | error: Validators.none, 195 | valid: true 196 | }; 197 | } 198 | 199 | } -------------------------------------------------------------------------------- /src/components/todo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, FocusEvent, MouseEvent } from "react" 2 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 3 | 4 | // Components 5 | import Layout from "../layout" 6 | import SEO from "../seo"; 7 | 8 | // Types 9 | import { Form } from "../../types/forms/Form" 10 | import { FormField } from "../../types/forms/FormField" 11 | import { IError } from "../../types/userbase/IError" 12 | import { ITodo } from "../../types/todo/itodo"; 13 | import { ITodoState } from "../../types/todo/itodo-state"; 14 | import { Validators } from "../../types/forms/Validators" 15 | 16 | class Todo extends React.Component<{ user: userbase.UserResult }, ITodoState> { 17 | constructor(props) { 18 | super(props); 19 | this.state = { 20 | user: this.props.user, 21 | todos: [], 22 | generalError: null, 23 | addTodoForm: new Form([ 24 | new FormField('todoName', '', [Validators.required]) 25 | ]), 26 | addTodoFormError: null 27 | }; 28 | this.handleDBChanges = this.handleDBChanges.bind(this); 29 | this.handleCompletionChange = this.handleCompletionChange.bind(this); 30 | this.handleInputChange = this.handleInputChange.bind(this); 31 | this.handleBlurEvent = this.handleBlurEvent.bind(this); 32 | this.handleSubmit = this.handleSubmit.bind(this); 33 | } 34 | 35 | componentDidMount() { 36 | userbase.openDatabase({ databaseName: 'todos', changeHandler: this.handleDBChanges }) 37 | .catch((error: IError) => { 38 | this.setState((state: ITodoState) => { 39 | return { 40 | ...state, 41 | generalError: error.message 42 | }; 43 | }); 44 | }); 45 | } 46 | 47 | /** 48 | * Updates the state on any changes to the database. 49 | * @param {Item[]} items 50 | */ 51 | handleDBChanges(items: userbase.Item[]) { 52 | this.setState((state) => { 53 | return { 54 | ...state, 55 | todos: items 56 | }; 57 | }); 58 | } 59 | 60 | /** 61 | * Updates the completed status of a todo. 62 | * @param {FormEvent} event 63 | * @param {ITodo} todo 64 | * @param {number} index 65 | */ 66 | handleCompletionChange(event: FormEvent, todo: ITodo, index: number) { 67 | event.persist(); 68 | let newTodos: userbase.Item[] = this.state.todos; 69 | newTodos[index].item = { 70 | ...newTodos[index].item, 71 | completed: !todo.completed 72 | }; 73 | this.setState((state) => { 74 | return { 75 | ...state, 76 | todos: newTodos 77 | }; 78 | }); 79 | const updatedTodo: ITodo = Object.assign({}, this.state.todos[index].item); 80 | const itemId: string = event.currentTarget.id; 81 | userbase.updateItem({ databaseName: 'todos', item: updatedTodo, itemId: itemId }) 82 | .catch((error: IError) => { 83 | this.setState((state: ITodoState) => { 84 | return { 85 | ...state, 86 | generalError: error.message 87 | }; 88 | }); 89 | }); 90 | } 91 | 92 | /** 93 | * Deletes a todo from the database. 94 | * @param {MouseEvent} event 95 | * @param {string} itemId 96 | * @param {number} index 97 | */ 98 | deleteTodo(event: MouseEvent, itemId: string, index: number) { 99 | event.persist(); 100 | let newTodos: userbase.Item[] = this.state.todos; 101 | newTodos.splice(index, 1); 102 | this.setState((state) => { 103 | return { 104 | ...state, 105 | todos: newTodos 106 | }; 107 | }); 108 | userbase.deleteItem({ databaseName: 'todos', itemId: itemId }) 109 | .catch((error: IError) => { 110 | this.setState((state: ITodoState) => { 111 | return { 112 | ...state, 113 | generalError: error.message 114 | }; 115 | }); 116 | }); 117 | } 118 | 119 | /** 120 | * Sets the form value in the state. 121 | * @param {FormEvent} event 122 | */ 123 | handleInputChange(event: FormEvent) { 124 | event.persist(); 125 | const newFormFieldValue: string = event.currentTarget.value; 126 | let addTodoForm: Form = this.state.addTodoForm; 127 | addTodoForm.setFormFieldValue('todoName', newFormFieldValue); 128 | this.setState((state: ITodoState) => { 129 | return { 130 | ...state, 131 | addTodoForm: addTodoForm 132 | }; 133 | }); 134 | } 135 | 136 | /** 137 | * Sets the touched attribute for a form field. 138 | * @param {FocusEvent} event 139 | */ 140 | handleBlurEvent(event: FocusEvent) { 141 | event.persist(); 142 | let addTodoForm: Form = this.state.addTodoForm; 143 | addTodoForm.setFormFieldTouched('todoName', true); 144 | this.setState((state: ITodoState) => { 145 | return { 146 | ...state, 147 | addTodoForm: addTodoForm 148 | }; 149 | }); 150 | } 151 | 152 | /** 153 | * Adds a new todo. 154 | * @param {FormEvent} event 155 | */ 156 | handleSubmit(event: FormEvent) { 157 | event.preventDefault(); 158 | let addTodoForm: Form = this.state.addTodoForm; 159 | addTodoForm.setSubmitted(true); 160 | this.setState((state: ITodoState) => { 161 | return { 162 | ...state, 163 | addTodoForm: addTodoForm 164 | }; 165 | }); 166 | if (this.state.addTodoForm.changed && this.state.addTodoForm.valid) { 167 | const todoName: string = this.state.addTodoForm.getFormField('todoName').value; 168 | userbase.insertItem({ databaseName: 'todos', item: { name: todoName, completed: false } }) 169 | .then(() => { 170 | let resetTodoForm: Form = this.state.addTodoForm; 171 | resetTodoForm.resetForm(); 172 | this.setState((state: ITodoState) => { 173 | return { 174 | ...state, 175 | addTodoForm: resetTodoForm 176 | }; 177 | }); 178 | }) 179 | .catch((error: IError) => { 180 | this.setState((state: ITodoState) => { 181 | return { 182 | ...state, 183 | addTodoFormError: error.message 184 | }; 185 | }); 186 | }); 187 | } 188 | } 189 | 190 | render() { 191 | return ( 192 | 193 | 194 |

Welcome, { this.state.user.profile?.firstName ? this.state.user.profile.firstName : this.state.user.username }!

195 |

Your TODOs

196 | { 197 | this.state.generalError && 198 |

{this.state.generalError}

199 | } 200 |
201 | { 202 | this.state.todos.map((todo: userbase.Item, index: number) => { 203 | return ( 204 |
205 | 208 | {this.handleCompletionChange(e, todo.item, index)}} /> 209 |

210 |
211 | ); 212 | }) 213 | } 214 |
215 |
216 |
217 | 218 |

219 | This field is required. 220 |

221 |
222 | 225 | { 226 | this.state.addTodoFormError && 227 |

{this.state.addTodoFormError}

228 | } 229 |
230 |
231 | ) 232 | } 233 | } 234 | 235 | export default Todo -------------------------------------------------------------------------------- /src/components/signup/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent, FocusEvent } from "react"; 2 | import { Link, navigate } from "gatsby"; 3 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 4 | 5 | // Components 6 | import Layout from "../layout"; 7 | import SEO from "../../components/seo"; 8 | 9 | // Types 10 | import { Form } from "../../types/forms/Form"; 11 | import { FormField } from "../../types/forms/FormField"; 12 | import { IError } from "../../types/userbase/IError"; 13 | import { ISignupDto } from "../../types/userbase/isignup-dto"; 14 | import { ISignupState } from "../../types/signup/isignup-state"; 15 | import { Validators } from "../../types/forms/Validators"; 16 | 17 | class Signup extends React.Component<{}, ISignupState> { 18 | constructor(props) { 19 | super(props); 20 | this.state = { 21 | signupForm: new Form([ 22 | new FormField('firstName', ''), 23 | new FormField('lastName', ''), 24 | new FormField('email', '', [Validators.required, Validators.email]), 25 | new FormField('username', '', [Validators.required]), 26 | new FormField('password', '', [Validators.required]), 27 | new FormField('confirmPassword', '', [Validators.required, Validators.matchFormField], 'password') 28 | ]), 29 | signupError: null 30 | }; 31 | this.handleInputChange = this.handleInputChange.bind(this); 32 | this.handleBlurEvent = this.handleBlurEvent.bind(this); 33 | this.handleSubmit = this.handleSubmit.bind(this); 34 | } 35 | 36 | /** 37 | * Sets the form value in the state. 38 | * @param {FormEvent} event 39 | */ 40 | handleInputChange(event: FormEvent) { 41 | event.persist(); 42 | const formFieldName: string = event.currentTarget.id; 43 | const newFormFieldValue: string = event.currentTarget.value; 44 | let signupForm: Form = this.state.signupForm; 45 | signupForm.setFormFieldValue(formFieldName, newFormFieldValue); 46 | this.setState((state: ISignupState) => { 47 | return { 48 | ...state, 49 | signupForm: signupForm 50 | }; 51 | }); 52 | } 53 | 54 | /** 55 | * Sets the touched attribute for a form field. 56 | * @param {FocusEvent} event 57 | */ 58 | handleBlurEvent(event: FocusEvent) { 59 | event.persist(); 60 | const formFieldName: string = event.target.id; 61 | let signupForm: Form = this.state.signupForm; 62 | signupForm.setFormFieldTouched(formFieldName, true); 63 | this.setState((state: ISignupState) => { 64 | return { 65 | ...state, 66 | signupForm: signupForm 67 | }; 68 | }); 69 | } 70 | 71 | /** 72 | * Signs the user up and sets the form to submitted. 73 | * @param {FormEvent} event 74 | */ 75 | handleSubmit(event: FormEvent) { 76 | event.preventDefault(); 77 | let signupForm: Form = this.state.signupForm; 78 | signupForm.setSubmitted(true); 79 | this.setState((state: ISignupState) => { 80 | return { 81 | ...state, 82 | signupForm: signupForm 83 | }; 84 | }); 85 | if (this.state.signupForm.valid) { 86 | let signupDto: ISignupDto = { 87 | username: this.state.signupForm.getFormField('username').value, 88 | password: this.state.signupForm.getFormField('password').value, 89 | email: this.state.signupForm.getFormField('email').value, 90 | profile: { 91 | firstName: null, 92 | lastName: null 93 | }, 94 | rememberMe: 'local' 95 | }; 96 | const firstName: string = this.state.signupForm.getFormField('firstName').value; 97 | const lastName: string = this.state.signupForm.getFormField('lastName').value; 98 | if (firstName && lastName) { 99 | signupDto.profile.firstName = firstName; 100 | signupDto.profile.lastName = lastName; 101 | } else if (firstName) { 102 | delete signupDto.profile.lastName; 103 | signupDto.profile.firstName = firstName; 104 | } else if (lastName) { 105 | delete signupDto.profile.firstName; 106 | signupDto.profile.lastName = lastName; 107 | } else { 108 | signupDto.profile = null; 109 | } 110 | userbase.signUp(signupDto) 111 | .then((user: userbase.UserResult) => { 112 | navigate('/app/dashboard'); 113 | }) 114 | .catch((error: IError) => { 115 | this.setState((state) => { 116 | return { 117 | ...state, 118 | loginError: error.message 119 | }; 120 | }); 121 | }); 122 | } 123 | } 124 | 125 | render() { 126 | return ( 127 | 128 | 129 |
130 |
131 |
132 |

Sign up for an account

133 |
134 |
135 | 138 | 139 |
140 |
141 | 144 | 145 |
146 |
147 | 150 | 151 |

152 | { this.state.signupForm.getFormField('email').error === Validators.required ? 'This field is required.' : '' } 153 | { this.state.signupForm.getFormField('email').error === Validators.email ? 'Please provide a valid email address.' : '' } 154 |

155 |
156 |
157 | 160 | 161 |

162 | { this.state.signupForm.getFormField('username').error === Validators.required ? 'This field is required.' : '' } 163 |

164 |
165 |
166 | 169 | 170 |

This field is required.

171 |
172 |
173 | 176 | 177 |

178 | { this.state.signupForm.getFormField('confirmPassword').error === Validators.required ? 'This field is required.' : '' } 179 | { this.state.signupForm.getFormField('confirmPassword').error === Validators.matchFormField ? 'Your passwords do not match.' : '' } 180 |

181 |
182 |
183 | 186 |
187 | { 188 | this.state.signupError && 189 |

{this.state.signupError}

190 | } 191 |
192 |
193 | 194 | Already have an account? Log in 195 | 196 |
197 |
198 |
199 |
200 | ); 201 | } 202 | } 203 | 204 | export default Signup; -------------------------------------------------------------------------------- /src/components/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEvent, FormEvent, FocusEvent } from "react"; 2 | import { navigate } from "gatsby"; 3 | import { Match } from "@reach/router"; 4 | const userbase = typeof window !== 'undefined' ? require('userbase-js').default : null; 5 | 6 | // Components 7 | import Layout from "../layout"; 8 | import SEO from "../seo"; 9 | 10 | // Types 11 | import { Form } from "../../types/forms/Form"; 12 | import { FormField } from "../../types/forms/FormField"; 13 | import { IError } from "../../types/userbase/IError"; 14 | import { IProfileState } from "../../types/profile/iprofile"; 15 | import { IUpdateAccountInfoDto } from "../../types/profile/iupdate-account-info-dto"; 16 | import { Validators } from "../../types/forms/Validators"; 17 | 18 | class Profile extends React.Component<{ user: userbase.UserResult }, IProfileState> { 19 | constructor(props) { 20 | super(props); 21 | this.state = { 22 | user: this.props.user, 23 | accountInfoForm: new Form([ 24 | new FormField('firstName', this.props.user.profile?.firstName), 25 | new FormField('lastName', this.props.user.profile?.lastName), 26 | new FormField('email', this.props.user.email, [Validators.required, Validators.email]) 27 | 28 | ]), 29 | accountInfoFormSuccess: false, 30 | accountInfoFormError: null, 31 | changePasswordForm: new Form([ 32 | new FormField('currentPassword', '', [Validators.required]), 33 | new FormField('newPassword', '', [Validators.required]), 34 | new FormField('confirmPassword', '', [Validators.required, Validators.matchFormField], 'newPassword') 35 | ]), 36 | changePasswordFormSuccess: false, 37 | changePasswordFormError: null, 38 | deleteAccountRequested: false, 39 | deleteAccountSuccess: false, 40 | deleteAccountError: null 41 | }; 42 | this.handleInputChange = this.handleInputChange.bind(this); 43 | this.handleBlurEvent = this.handleBlurEvent.bind(this); 44 | this.handleSubmit = this.handleSubmit.bind(this); 45 | this.deleteAccount = this.deleteAccount.bind(this); 46 | } 47 | 48 | /** 49 | * Sets the form value in the state. 50 | * @param {FormEvent} event 51 | * @param {string} formName 52 | */ 53 | handleInputChange(event: FormEvent, formName: string) { 54 | event.persist(); 55 | const formFieldName: string = event.currentTarget.id; 56 | const newFormFieldValue: string = event.currentTarget.value; 57 | let profileForm: Form = this.state[formName]; 58 | profileForm.setFormFieldValue(formFieldName, newFormFieldValue); 59 | this.setState((state: IProfileState) => { 60 | return { 61 | ...state, 62 | [formName]: profileForm 63 | }; 64 | }); 65 | } 66 | 67 | /** 68 | * Sets the touched attribute for a form field. 69 | * @param {FocusEvent} event 70 | * @param {string} formName 71 | */ 72 | handleBlurEvent(event: FocusEvent, formName: string) { 73 | event.persist(); 74 | const formFieldName: string = event.target.id; 75 | let profileForm: Form = this.state[formName]; 76 | profileForm.setFormFieldTouched(formFieldName, true); 77 | this.setState((state: IProfileState) => { 78 | return { 79 | ...state, 80 | [formName]: profileForm 81 | }; 82 | }); 83 | } 84 | 85 | /** 86 | * Signs the user up and sets the form to submitted. 87 | * @param {FormEvent} event 88 | * @param {string} formName 89 | */ 90 | handleSubmit(event: FormEvent, formName: string) { 91 | event.preventDefault(); 92 | let profileForm: Form = this.state[formName]; 93 | profileForm.setSubmitted(true); 94 | this.setState((state: IProfileState) => { 95 | return { 96 | ...state, 97 | [formName]: profileForm 98 | }; 99 | }); 100 | if (formName === 'accountInfoForm') { 101 | if (this.state.accountInfoForm.valid && this.state.accountInfoForm.changed) { 102 | const firstName: string = this.state.accountInfoForm.getFormField('firstName').value; 103 | const lastName: string = this.state.accountInfoForm.getFormField('lastName').value; 104 | let updateAccountInfoDto: IUpdateAccountInfoDto = { 105 | username: this.state.user.username, 106 | email: this.state.accountInfoForm.getFormField('email').value, 107 | profile: { 108 | firstName: null, 109 | lastName: null 110 | } 111 | }; 112 | if (firstName && lastName) { 113 | updateAccountInfoDto.profile.firstName = firstName; 114 | updateAccountInfoDto.profile.lastName = lastName; 115 | } else if (firstName) { 116 | delete updateAccountInfoDto.profile.lastName; 117 | updateAccountInfoDto.profile.firstName = firstName; 118 | } else if (lastName) { 119 | delete updateAccountInfoDto.profile.firstName; 120 | updateAccountInfoDto.profile.lastName = lastName; 121 | } else { 122 | updateAccountInfoDto.profile = null; 123 | } 124 | userbase.updateUser(updateAccountInfoDto) 125 | .then(() => { 126 | this.setState((state) => { 127 | return { 128 | ...state, 129 | accountInfoFormSuccess: true, 130 | accountInfoFormError: null 131 | }; 132 | }); 133 | }) 134 | .catch((error: IError) => { 135 | this.setState((state) => { 136 | return { 137 | ...state, 138 | accountInfoFormSuccess: false, 139 | accountInfoFormError: error.message 140 | }; 141 | }); 142 | }); 143 | } 144 | } else { 145 | if (this.state.changePasswordForm.valid && this.state.changePasswordForm.changed) { 146 | const updateAccountInfoDto: IUpdateAccountInfoDto = { 147 | username: this.state.user.username, 148 | currentPassword: this.state.changePasswordForm.getFormField('currentPassword').value, 149 | newPassword: this.state.changePasswordForm.getFormField('newPassword').value 150 | }; 151 | userbase.updateUser(updateAccountInfoDto) 152 | .then(() => { 153 | let changePasswordForm: Form = this.state.changePasswordForm; 154 | changePasswordForm.resetForm(); 155 | this.setState((state) => { 156 | return { 157 | ...state, 158 | changePasswordForm: changePasswordForm, 159 | changePasswordFormSuccess: true, 160 | changePasswordFormError: null 161 | }; 162 | }); 163 | }) 164 | .catch((error: IError) => { 165 | this.setState((state) => { 166 | return { 167 | ...state, 168 | changePasswordFormSuccess: false, 169 | changePasswordFormError: error.message 170 | }; 171 | }); 172 | }); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Deletes a user's account and sends them to the home page upon deletion. 179 | * @param {MouseEvent} event 180 | */ 181 | deleteAccount(event: MouseEvent) { 182 | userbase.deleteUser() 183 | .then(() => { 184 | this.setState((state) => { 185 | return { 186 | ...state, 187 | deleteAccountSuccess: true, 188 | deleteAccountError: null 189 | }; 190 | }); 191 | navigate('/'); 192 | }) 193 | .catch((error: IError) => { 194 | this.setState((state) => { 195 | return { 196 | ...state, 197 | deleteAccountSuccess: false, 198 | deleteAccountError: error.message 199 | }; 200 | }); 201 | }); 202 | } 203 | 204 | render() { 205 | return ( 206 | 207 | 208 |
209 | 210 | { 211 | props => (props.match && !this.state.changePasswordFormSuccess) ? 212 |
213 |

214 | Please change your password before continuing. The temporary password you used 215 | is only valid for 24 hours after you submitted your forgot password request. 216 |

217 |
: 218 | '' 219 | } 220 |
221 |
222 |

Profile

223 |

Account Info

224 |
this.handleSubmit(e, 'accountInfoForm')}> 225 |
226 | 229 | this.handleInputChange(e, 'accountInfoForm')} /> 230 |
231 |
232 | 235 | this.handleInputChange(e, 'accountInfoForm')} /> 236 |
237 |
238 | 241 | this.handleInputChange(e, 'accountInfoForm')} onBlur={(e) => this.handleBlurEvent(e, 'accountInfoForm')} /> 242 |

243 | { this.state.accountInfoForm.getFormField('email').error === Validators.required ? 'This field is required.' : '' } 244 | { this.state.accountInfoForm.getFormField('email').error === Validators.email ? 'Please provide a valid email address.' : '' } 245 |

246 |
247 | 250 | { 251 | this.state.accountInfoFormSuccess && 252 |

Your account info was successfully updated!

253 | } 254 | { 255 | this.state.accountInfoFormError && 256 |

{this.state.accountInfoFormError}

257 | } 258 |
259 |
260 |

Change Password

261 |
this.handleSubmit(e, 'changePasswordForm')}> 262 |
263 | 266 | this.handleInputChange(e, 'changePasswordForm')} /> 267 |

This field is required.

268 |
269 |
270 | 273 | this.handleInputChange(e, 'changePasswordForm')} /> 274 |

This field is required.

275 |
276 |
277 | 280 | this.handleInputChange(e, 'changePasswordForm')} onBlur={(e) => this.handleBlurEvent(e, 'changePasswordForm')} /> 281 |

282 | { this.state.changePasswordForm.getFormField('confirmPassword').error === Validators.required ? 'This field is required.' : '' } 283 | { this.state.changePasswordForm.getFormField('confirmPassword').error === Validators.matchFormField ? 'Your passwords do not match.' : '' } 284 |

285 |
286 | 289 | { 290 | this.state.changePasswordFormSuccess && 291 |

Your password was successfully changed!

292 | } 293 | { 294 | this.state.changePasswordFormError && 295 |

{this.state.changePasswordFormError}

296 | } 297 |
298 |
299 |

Danger Zone

300 |

Be advised that by performing the action, your account is not recoverable.

301 |
302 | 305 | { 306 | this.state.deleteAccountRequested && 307 | 310 | } 311 |
312 |
313 |
314 |
315 | ) 316 | } 317 | } 318 | 319 | export default Profile --------------------------------------------------------------------------------