├── .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 [](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 |
23 |
24 |
25 | Todos
26 |
27 |
28 |
29 |
30 | Profile
31 |
32 |
33 |
34 | Sign out
35 |
36 | :
37 |
38 |
39 | Login
40 |
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 |
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 |
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 | this.deleteTodo(e, todo.itemId, index)}>
206 | X
207 |
208 | {this.handleCompletionChange(e, todo.item, index)}} />
209 | { todo.item.name }
210 |
211 | );
212 | })
213 | }
214 |
215 |
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 |
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 |
259 |
260 |
Change Password
261 |
298 |
299 |
Danger Zone
300 |
Be advised that by performing the action, your account is not recoverable.
301 |
302 | this.setState(state => {return {...state, deleteAccountRequested: true }})}>
303 | Delete Account
304 |
305 | {
306 | this.state.deleteAccountRequested &&
307 |
308 | Confirm Delete Account
309 |
310 | }
311 |
312 |
313 |
314 |
315 | )
316 | }
317 | }
318 |
319 | export default Profile
--------------------------------------------------------------------------------