├── .gitignore
├── LICENSE
├── README.md
├── netlify.toml
├── package-lock.json
├── package.json
├── public
├── android-chrome-192x192.png
├── android-chrome-256x256.png
├── apple-touch-icon.png
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── index.html
├── manifest.json
├── mstile-150x150.png
├── robots.txt
├── safari-pinned-tab.svg
└── site.webmanifest
└── src
├── App.css
├── App.js
├── App.test.js
├── Routes.js
├── components
├── AppliedRoute.js
├── AuthenticatedRoute.js
├── BillingForm.css
├── BillingForm.js
├── ErrorBoundary.css
├── ErrorBoundary.js
├── LoaderButton.css
├── LoaderButton.js
├── RouteNavItem.js
└── UnauthenticatedRoute.js
├── config.js
├── containers
├── ChangeEmail.css
├── ChangeEmail.js
├── ChangePassword.css
├── ChangePassword.js
├── Home.css
├── Home.js
├── Login.css
├── Login.js
├── NewNote.css
├── NewNote.js
├── NotFound.css
├── NotFound.js
├── Notes.css
├── Notes.js
├── ResetPassword.css
├── ResetPassword.js
├── Settings.css
├── Settings.js
├── Signup.css
└── Signup.js
├── index.css
├── index.js
├── libs
├── awsLib.js
├── contextLib.js
├── errorLib.js
└── hooksLib.js
└── serviceWorker.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 |
6 | # testing
7 | /coverage
8 |
9 | # production
10 | /build
11 |
12 | # misc
13 | .DS_Store
14 | .env
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Anomaly Innovations
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Serverless Stack User Management Demo React App
2 |
3 | [Serverless Stack](http://serverless-stack.com) is a free comprehensive guide to creating full-stack serverless applications. We create a [note taking app](https://demo.serverless-stack.com) from scratch.
4 |
5 | This repo is [a fork of the original client](https://github.com/AnomalyInnovations/serverless-stack-demo-client) with added support for a Login with Facebook option. You can visit [a hosted version of this app here](https://demo-user-mgmt.serverless-stack.com).
6 |
7 | #### Usage
8 |
9 | This project is created using [Create React App](https://github.com/facebookincubator/create-react-app).
10 |
11 | To use this repo locally, start by cloning it and installing the NPM packages.
12 |
13 | ``` bash
14 | $ git clone https://github.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client
15 | $ npm install
16 | ```
17 |
18 | Run it locally.
19 |
20 | ``` bash
21 | $ npm run start
22 | ```
23 |
24 | #### Maintainers
25 |
26 | This repo is maintained by [Serverless Stack](https://serverless-stack.com).
27 |
28 | [Email]: mailto:hello@serverless-stack.com
29 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | # Global settings applied to the whole site.
2 | # “base” is directory to change to before starting build, and
3 | # “publish” is the directory to publish (relative to root of your repo).
4 | # “command” is your build command.
5 |
6 | [build]
7 | base = ""
8 | publish = "build"
9 | command = "REACT_APP_STAGE=dev npm run build"
10 |
11 | # Production context: All deploys to the main
12 | # repository branch will inherit these settings.
13 | [context.production]
14 | command = "REACT_APP_STAGE=prod npm run build"
15 |
16 | # Deploy Preview context: All Deploy Previews
17 | # will inherit these settings.
18 | [context.deploy-preview]
19 | command = "REACT_APP_STAGE=dev npm run build"
20 |
21 | # Branch Deploy context: All deploys that are not in
22 | # an active Deploy Preview will inherit these settings.
23 | [context.branch-deploy]
24 | command = "REACT_APP_STAGE=dev npm run build"
25 |
26 | # Always redirect any request to our index.html
27 | # and return the status code 200.
28 | [[redirects]]
29 | from = "/*"
30 | to = "/index.html"
31 | status = 200
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notes-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@sentry/browser": "^5.15.4",
7 | "aws-amplify": "^3.0.6",
8 | "react": "^16.13.1",
9 | "react-bootstrap": "^0.33.1",
10 | "react-dom": "^16.13.1",
11 | "react-router-bootstrap": "^0.25.0",
12 | "react-router-dom": "^5.1.2",
13 | "react-scripts": "3.4.1",
14 | "react-stripe-elements": "^6.1.1"
15 | },
16 | "scripts": {
17 | "start": "react-scripts start",
18 | "build": "react-scripts build",
19 | "test": "react-scripts test",
20 | "eject": "react-scripts eject"
21 | },
22 | "eslintConfig": {
23 | "extends": "react-app"
24 | },
25 | "browserslist": {
26 | "production": [
27 | ">0.2%",
28 | "not dead",
29 | "not op_mini all"
30 | ],
31 | "development": [
32 | "last 1 chrome version",
33 | "last 1 firefox version",
34 | "last 1 safari version"
35 | ]
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client/4c723d9501d679dbb5e6ef650021f4bed69c6917/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client/4c723d9501d679dbb5e6ef650021f4bed69c6917/public/android-chrome-256x256.png
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client/4c723d9501d679dbb5e6ef650021f4bed69c6917/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client/4c723d9501d679dbb5e6ef650021f4bed69c6917/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client/4c723d9501d679dbb5e6ef650021f4bed69c6917/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client/4c723d9501d679dbb5e6ef650021f4bed69c6917/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
16 |
17 |
18 |
19 |
28 | Scratch - A simple note taking app
29 |
30 |
31 |
32 |
33 |
34 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Scratch",
3 | "name": "Scratch Note Taking App",
4 | "icons": [
5 | {
6 | "src": "android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "android-chrome-256x256.png",
12 | "sizes": "256x256",
13 | "type": "image/png"
14 | }
15 | ],
16 | "start_url": ".",
17 | "display": "standalone",
18 | "theme_color": "#ffffff",
19 | "background_color": "#ffffff"
20 | }
21 |
--------------------------------------------------------------------------------
/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AnomalyInnovations/serverless-stack-demo-user-mgmt-client/4c723d9501d679dbb5e6ef650021f4bed69c6917/public/mstile-150x150.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/public/safari-pinned-tab.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
23 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-256x256.png",
12 | "sizes": "256x256",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#ffffff",
17 | "background_color": "#ffffff",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | margin-top: 15px;
3 | }
4 |
5 | .App .navbar-brand {
6 | font-weight: bold;
7 | }
8 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { Auth } from "aws-amplify";
3 | import { Link, useHistory } from "react-router-dom";
4 | import { Nav, Navbar, NavItem } from "react-bootstrap";
5 | import { LinkContainer } from "react-router-bootstrap";
6 | import ErrorBoundary from "./components/ErrorBoundary";
7 | import { AppContext } from "./libs/contextLib";
8 | import { onError } from "./libs/errorLib";
9 | import Routes from "./Routes";
10 | import "./App.css";
11 |
12 | function App() {
13 | const history = useHistory();
14 | const [isAuthenticating, setIsAuthenticating] = useState(true);
15 | const [isAuthenticated, userHasAuthenticated] = useState(false);
16 |
17 | useEffect(() => {
18 | onLoad();
19 | }, []);
20 |
21 | async function onLoad() {
22 | try {
23 | await Auth.currentSession();
24 | userHasAuthenticated(true);
25 | }
26 | catch(e) {
27 | if (e !== 'No current user') {
28 | onError(e);
29 | }
30 | }
31 |
32 | setIsAuthenticating(false);
33 | }
34 |
35 | async function handleLogout() {
36 | await Auth.signOut();
37 |
38 | userHasAuthenticated(false);
39 |
40 | history.push("/login");
41 | }
42 |
43 | return (
44 | !isAuthenticating && (
45 |
46 |
47 |
48 |
49 | Scratch
50 |
51 |
52 |
53 |
54 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | )
82 | );
83 | }
84 |
85 | export default App;
86 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/Routes.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Switch } from "react-router-dom";
3 |
4 | import AuthenticatedRoute from "./components/AuthenticatedRoute";
5 | import UnauthenticatedRoute from "./components/UnauthenticatedRoute";
6 |
7 | import Home from "./containers/Home";
8 | import Login from "./containers/Login";
9 | import Notes from "./containers/Notes";
10 | import Signup from "./containers/Signup";
11 | import NewNote from "./containers/NewNote";
12 | import Settings from "./containers/Settings";
13 | import NotFound from "./containers/NotFound";
14 | import ChangeEmail from "./containers/ChangeEmail";
15 | import ResetPassword from "./containers/ResetPassword";
16 | import ChangePassword from "./containers/ChangePassword";
17 |
18 | export default function Routes() {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | {/* Finally, catch all unmatched routes */}
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/src/components/AppliedRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route } from "react-router-dom";
3 |
4 | export default function AppliedRoute({ component: C, appProps, ...rest }) {
5 | return (
6 | } />
7 | );
8 | }
9 |
--------------------------------------------------------------------------------
/src/components/AuthenticatedRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Redirect, useLocation } from "react-router-dom";
3 | import { useAppContext } from "../libs/contextLib";
4 |
5 | export default function AuthenticatedRoute({ children, ...rest }) {
6 | const { pathname, search } = useLocation();
7 | const { isAuthenticated } = useAppContext();
8 | return (
9 |
10 | {isAuthenticated ? (
11 | children
12 | ) : (
13 |
16 | )}
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/BillingForm.css:
--------------------------------------------------------------------------------
1 | .BillingForm .card-field {
2 | margin-bottom: 15px;
3 | background-color: white;
4 | padding: 11px 16px;
5 | border-radius: 6px;
6 | border: 1px solid #CCC;
7 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
8 | line-height: 1.3333333;
9 | }
10 |
11 | .BillingForm .card-field.StripeElement--focus {
12 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 8px rgba(102, 175, 233, .6);
13 | border-color: #66AFE9;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/BillingForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FormGroup, FormControl, ControlLabel } from "react-bootstrap";
3 | import { CardElement, injectStripe } from "react-stripe-elements";
4 | import LoaderButton from "./LoaderButton";
5 | import { useFormFields } from "../libs/hooksLib";
6 | import "./BillingForm.css";
7 |
8 | function BillingForm({ isLoading, onSubmit, ...props }) {
9 | const [fields, handleFieldChange] = useFormFields({
10 | name: "",
11 | storage: ""
12 | });
13 | const [isProcessing, setIsProcessing] = useState(false);
14 | const [isCardComplete, setIsCardComplete] = useState(false);
15 |
16 | isLoading = isProcessing || isLoading;
17 |
18 | function validateForm() {
19 | return (
20 | fields.name !== "" &&
21 | fields.storage !== "" &&
22 | isCardComplete
23 | );
24 | }
25 |
26 | async function handleSubmitClick(event) {
27 | event.preventDefault();
28 |
29 | setIsProcessing(true);
30 |
31 | const { token, error } = await props.stripe.createToken({ name: fields.name });
32 |
33 | setIsProcessing(false);
34 |
35 | onSubmit(fields.storage, { token, error });
36 | }
37 |
38 | return (
39 |
78 | );
79 | }
80 |
81 | export default injectStripe(BillingForm);
82 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary.css:
--------------------------------------------------------------------------------
1 | .ErrorBoundary {
2 | padding-top: 100px;
3 | text-align: center;
4 | }
5 |
--------------------------------------------------------------------------------
/src/components/ErrorBoundary.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { logError } from "../libs/errorLib";
3 | import "./ErrorBoundary.css";
4 |
5 | export default class ErrorBoundary extends React.Component {
6 | state = { hasError: false };
7 |
8 | static getDerivedStateFromError(error) {
9 | return { hasError: true };
10 | }
11 |
12 | componentDidCatch(error, errorInfo) {
13 | logError(error, errorInfo);
14 | }
15 |
16 | render() {
17 | return this.state.hasError ? (
18 |
19 |
Sorry there was a problem loading this page
20 |
21 | ) : (
22 | this.props.children
23 | );
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/LoaderButton.css:
--------------------------------------------------------------------------------
1 | .LoaderButton .spinning.glyphicon {
2 | margin-right: 7px;
3 | top: 2px;
4 | animation: spin 1s infinite linear;
5 | }
6 | @keyframes spin {
7 | from { transform: scale(1) rotate(0deg); }
8 | to { transform: scale(1) rotate(360deg); }
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/LoaderButton.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Button, Glyphicon } from "react-bootstrap";
3 | import "./LoaderButton.css";
4 |
5 | export default function LoaderButton({
6 | isLoading,
7 | className = "",
8 | disabled = false,
9 | ...props
10 | }) {
11 | return (
12 |
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/RouteNavItem.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route } from "react-router-dom";
3 | import { NavItem } from "react-bootstrap";
4 |
5 | export default props =>
6 |
10 | history.push(e.currentTarget.getAttribute("href"))}
12 | {...props}
13 | active={match ? true : false}
14 | >
15 | {props.children}
16 | }
17 | />;
18 |
--------------------------------------------------------------------------------
/src/components/UnauthenticatedRoute.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Redirect } from "react-router-dom";
3 | import { useAppContext } from "../libs/contextLib";
4 |
5 | function querystring(name, url = window.location.href) {
6 | name = name.replace(/[[]]/g, "\\$&");
7 |
8 | const regex = new RegExp("[?&]" + name + "(=([^]*)|&|#|$)", "i");
9 | const results = regex.exec(url);
10 |
11 | if (!results) {
12 | return null;
13 | }
14 | if (!results[2]) {
15 | return "";
16 | }
17 |
18 | return decodeURIComponent(results[2].replace(/\+/g, " "));
19 | }
20 |
21 | export default function UnauthenticatedRoute({ children, ...rest }) {
22 | const { isAuthenticated } = useAppContext();
23 | const redirect = querystring("redirect");
24 | return (
25 |
26 | {!isAuthenticated ? (
27 | children
28 | ) : (
29 |
30 | )}
31 |
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | const dev = {
2 | STRIPE_KEY: "pk_test_v1amvR35uoCNduJfkqGB8RLD",
3 | s3: {
4 | REGION: "us-east-1",
5 | BUCKET: "notes-app-2-api-dev-attachmentsbucket-qvdowkch2uj1"
6 | },
7 | apiGateway: {
8 | REGION: "us-east-1",
9 | URL: "https://api.serverless-stack.seed-demo.club/dev"
10 | },
11 | cognito: {
12 | REGION: "us-east-1",
13 | USER_POOL_ID: "us-east-1_f8l0OHPtp",
14 | APP_CLIENT_ID: "5r5o292r1n7s2vgje5u5c7vpq0",
15 | IDENTITY_POOL_ID: "us-east-1:b718098b-fe72-410b-b4c6-0750f9135672"
16 | }
17 | };
18 |
19 | const prod = {
20 | STRIPE_KEY: "pk_test_v1amvR35uoCNduJfkqGB8RLD",
21 | s3: {
22 | REGION: "us-east-1",
23 | BUCKET: "notes-app-2-api-prod-attachmentsbucket-1v9w7kkxnznb7"
24 | },
25 | apiGateway: {
26 | REGION: "us-east-1",
27 | URL: "https://api.serverless-stack.seed-demo.club/prod"
28 | },
29 | cognito: {
30 | REGION: "us-east-1",
31 | USER_POOL_ID: "us-east-1_mLbfKylhm",
32 | APP_CLIENT_ID: "mli2vaupiq3ga29m4698m6mrl",
33 | IDENTITY_POOL_ID: "us-east-1:4e377eff-0617-4098-b218-673490ffab8d"
34 | }
35 | };
36 |
37 | // Default to dev if not set
38 | const config = process.env.REACT_APP_STAGE === 'prod'
39 | ? prod
40 | : dev;
41 |
42 | export default {
43 | // Add common config values here
44 | MAX_ATTACHMENT_SIZE: 5000000,
45 | ...config
46 | };
47 |
--------------------------------------------------------------------------------
/src/containers/ChangeEmail.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 480px) {
2 | .ChangeEmail {
3 | padding: 60px 0;
4 | }
5 |
6 | .ChangeEmail form {
7 | margin: 0 auto;
8 | max-width: 320px;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/containers/ChangeEmail.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Auth } from "aws-amplify";
3 | import { useHistory } from "react-router-dom";
4 | import {
5 | HelpBlock,
6 | FormGroup,
7 | FormControl,
8 | ControlLabel,
9 | } from "react-bootstrap";
10 | import LoaderButton from "../components/LoaderButton";
11 | import { useFormFields } from "../libs/hooksLib";
12 | import { onError } from "../libs/errorLib";
13 | import "./ChangeEmail.css";
14 |
15 | export default function ChangeEmail() {
16 | const history = useHistory();
17 | const [codeSent, setCodeSent] = useState(false);
18 | const [fields, handleFieldChange] = useFormFields({
19 | code: "",
20 | email: "",
21 | });
22 | const [isConfirming, setIsConfirming] = useState(false);
23 | const [isSendingCode, setIsSendingCode] = useState(false);
24 |
25 | function validateEmailForm() {
26 | return fields.email.length > 0;
27 | }
28 |
29 | function validateConfirmForm() {
30 | return fields.code.length > 0;
31 | }
32 |
33 | async function handleUpdateClick(event) {
34 | event.preventDefault();
35 |
36 | setIsSendingCode(true);
37 |
38 | try {
39 | const user = await Auth.currentAuthenticatedUser();
40 | await Auth.updateUserAttributes(user, { email: fields.email });
41 | setCodeSent(true);
42 | } catch (error) {
43 | onError(error);
44 | setIsSendingCode(false);
45 | }
46 | }
47 |
48 | async function handleConfirmClick(event) {
49 | event.preventDefault();
50 |
51 | setIsConfirming(true);
52 |
53 | try {
54 | await Auth.verifyCurrentUserAttributeSubmit("email", fields.code);
55 |
56 | history.push("/settings");
57 | } catch (error) {
58 | onError(error);
59 | setIsConfirming(false);
60 | }
61 | }
62 |
63 | function renderUpdateForm() {
64 | return (
65 |
85 | );
86 | }
87 |
88 | function renderConfirmationForm() {
89 | return (
90 |
113 | );
114 | }
115 |
116 | return (
117 |
118 | {!codeSent ? renderUpdateForm() : renderConfirmationForm()}
119 |
120 | );
121 | }
122 |
--------------------------------------------------------------------------------
/src/containers/ChangePassword.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 480px) {
2 | .ChangePassword {
3 | padding: 60px 0;
4 | }
5 |
6 | .ChangePassword form {
7 | margin: 0 auto;
8 | max-width: 320px;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/containers/ChangePassword.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Auth } from "aws-amplify";
3 | import { useHistory } from "react-router-dom";
4 | import { FormGroup, FormControl, ControlLabel } from "react-bootstrap";
5 | import LoaderButton from "../components/LoaderButton";
6 | import { useFormFields } from "../libs/hooksLib";
7 | import { onError } from "../libs/errorLib";
8 | import "./ChangePassword.css";
9 |
10 | export default function ChangePassword() {
11 | const history = useHistory();
12 | const [fields, handleFieldChange] = useFormFields({
13 | password: "",
14 | oldPassword: "",
15 | confirmPassword: "",
16 | });
17 | const [isChanging, setIsChanging] = useState(false);
18 |
19 | function validateForm() {
20 | return (
21 | fields.oldPassword.length > 0 &&
22 | fields.password.length > 0 &&
23 | fields.password === fields.confirmPassword
24 | );
25 | }
26 |
27 | async function handleChangeClick(event) {
28 | event.preventDefault();
29 |
30 | setIsChanging(true);
31 |
32 | try {
33 | const currentUser = await Auth.currentAuthenticatedUser();
34 | await Auth.changePassword(
35 | currentUser,
36 | fields.oldPassword,
37 | fields.password
38 | );
39 |
40 | history.push("/settings");
41 | } catch (error) {
42 | onError(error);
43 | setIsChanging(false);
44 | }
45 | }
46 |
47 | return (
48 |
49 |
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/src/containers/Home.css:
--------------------------------------------------------------------------------
1 | .Home .lander {
2 | padding: 80px 0;
3 | text-align: center;
4 | }
5 |
6 | .Home .lander h1 {
7 | font-family: "Open Sans", sans-serif;
8 | font-weight: 600;
9 | }
10 |
11 | .Home .lander p {
12 | color: #999;
13 | }
14 |
15 | .Home .notes h4 {
16 | font-family: "Open Sans", sans-serif;
17 | font-weight: 600;
18 | overflow: hidden;
19 | line-height: 1.5;
20 | white-space: nowrap;
21 | text-overflow: ellipsis;
22 | }
23 | .Home .notes p {
24 | color: #666;
25 | }
26 |
27 | .Home .lander div {
28 | padding-top: 20px;
29 | }
30 | .Home .lander div a:first-child {
31 | margin-right: 20px;
32 | }
33 |
--------------------------------------------------------------------------------
/src/containers/Home.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { API } from "aws-amplify";
3 | import { Link } from "react-router-dom";
4 | import { LinkContainer } from "react-router-bootstrap";
5 | import { PageHeader, ListGroup, ListGroupItem } from "react-bootstrap";
6 | import { useAppContext } from "../libs/contextLib";
7 | import { onError } from "../libs/errorLib";
8 | import "./Home.css";
9 |
10 |
11 | export default function Home() {
12 | const [notes, setNotes] = useState([]);
13 | const { isAuthenticated } = useAppContext();
14 | const [isLoading, setIsLoading] = useState(true);
15 |
16 | useEffect(() => {
17 | async function onLoad() {
18 | if (!isAuthenticated) {
19 | return;
20 | }
21 |
22 | try {
23 | const notes = await loadNotes();
24 | setNotes(notes);
25 | } catch (e) {
26 | onError(e);
27 | }
28 |
29 | setIsLoading(false);
30 | }
31 |
32 | onLoad();
33 | }, [isAuthenticated]);
34 |
35 | function loadNotes() {
36 | return API.get("notes", "/notes");
37 | }
38 |
39 | function renderNotesList(notes) {
40 | return [{}].concat(notes).map((note, i) =>
41 | i !== 0 ? (
42 |
43 |
44 | {"Created: " + new Date(note.createdAt).toLocaleString()}
45 |
46 |
47 | ) : (
48 |
49 |
50 |
51 | {"\uFF0B"} Create a new note
52 |
53 |
54 |
55 | )
56 | );
57 | }
58 |
59 | function renderLander() {
60 | return (
61 |
62 |
Scratch
63 |
A simple note taking app
64 |
65 |
66 | Login
67 |
68 |
69 | Signup
70 |
71 |
72 |
73 | );
74 | }
75 |
76 | function renderNotes() {
77 | return (
78 |
79 |
Your Notes
80 |
81 | {!isLoading && renderNotesList(notes)}
82 |
83 |
84 | );
85 | }
86 |
87 | return (
88 |
89 | {isAuthenticated ? renderNotes() : renderLander()}
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/src/containers/Login.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 480px) {
2 | .Login {
3 | padding: 60px 0;
4 | }
5 |
6 | .Login form {
7 | margin: 0 auto;
8 | max-width: 320px;
9 | }
10 | }
11 |
12 | .Login form a {
13 | margin-bottom: 15px;
14 | display: block;
15 | font-size: 14px;
16 | }
17 |
--------------------------------------------------------------------------------
/src/containers/Login.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Auth } from "aws-amplify";
3 | import { Link } from "react-router-dom";
4 | import { FormGroup, FormControl, ControlLabel } from "react-bootstrap";
5 | import LoaderButton from "../components/LoaderButton";
6 | import { useAppContext } from "../libs/contextLib";
7 | import { useFormFields } from "../libs/hooksLib";
8 | import { onError } from "../libs/errorLib";
9 | import "./Login.css";
10 |
11 | export default function Login() {
12 | const { userHasAuthenticated } = useAppContext();
13 | const [isLoading, setIsLoading] = useState(false);
14 | const [fields, handleFieldChange] = useFormFields({
15 | email: "",
16 | password: ""
17 | });
18 |
19 | function validateForm() {
20 | return fields.email.length > 0 && fields.password.length > 0;
21 | }
22 |
23 | async function handleSubmit(event) {
24 | event.preventDefault();
25 |
26 | setIsLoading(true);
27 |
28 | try {
29 | await Auth.signIn(fields.email, fields.password);
30 | userHasAuthenticated(true);
31 | } catch (e) {
32 | onError(e);
33 | setIsLoading(false);
34 | }
35 | }
36 |
37 | return (
38 |
39 |
68 |
69 | );
70 | }
71 |
--------------------------------------------------------------------------------
/src/containers/NewNote.css:
--------------------------------------------------------------------------------
1 | .NewNote form {
2 | padding-bottom: 15px;
3 | }
4 |
5 | .NewNote form textarea {
6 | height: 300px;
7 | font-size: 24px;
8 | }
9 |
--------------------------------------------------------------------------------
/src/containers/NewNote.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from "react";
2 | import { API } from "aws-amplify";
3 | import { useHistory } from "react-router-dom";
4 | import { FormGroup, FormControl, ControlLabel } from "react-bootstrap";
5 | import LoaderButton from "../components/LoaderButton";
6 | import { onError } from "../libs/errorLib";
7 | import { s3Upload } from "../libs/awsLib";
8 | import config from "../config";
9 | import "./NewNote.css";
10 |
11 | export default function NewNote() {
12 | const file = useRef(null);
13 | const history = useHistory();
14 | const [content, setContent] = useState("");
15 | const [isLoading, setIsLoading] = useState(false);
16 |
17 | function validateForm() {
18 | return content.length > 0;
19 | }
20 |
21 | function handleFileChange(event) {
22 | file.current = event.target.files[0];
23 | }
24 |
25 | async function handleSubmit(event) {
26 | event.preventDefault();
27 |
28 | if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) {
29 | alert(
30 | `Please pick a file smaller than ${
31 | config.MAX_ATTACHMENT_SIZE / 1000000
32 | } MB.`
33 | );
34 | return;
35 | }
36 |
37 | setIsLoading(true);
38 |
39 | try {
40 | const attachment = file.current ? await s3Upload(file.current) : null;
41 |
42 | await createNote({ content, attachment });
43 | history.push("/");
44 | } catch (e) {
45 | onError(e);
46 | setIsLoading(false);
47 | }
48 | }
49 |
50 | function createNote(note) {
51 | return API.post("notes", "/notes", {
52 | body: note
53 | });
54 | }
55 |
56 | return (
57 |
58 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/src/containers/NotFound.css:
--------------------------------------------------------------------------------
1 | .NotFound {
2 | padding-top: 100px;
3 | text-align: center;
4 | }
5 |
--------------------------------------------------------------------------------
/src/containers/NotFound.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./NotFound.css";
3 |
4 | export default function NotFound() {
5 | return (
6 |
7 |
Sorry, page not found!
8 |
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/src/containers/Notes.css:
--------------------------------------------------------------------------------
1 | .Notes form {
2 | padding-bottom: 15px;
3 | }
4 |
5 | .Notes form textarea {
6 | height: 300px;
7 | font-size: 24px;
8 | }
9 |
--------------------------------------------------------------------------------
/src/containers/Notes.js:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState, useEffect } from "react";
2 | import { API, Storage } from "aws-amplify";
3 | import { useParams, useHistory } from "react-router-dom";
4 | import { FormGroup, FormControl, ControlLabel } from "react-bootstrap";
5 | import LoaderButton from "../components/LoaderButton";
6 | import { onError } from "../libs/errorLib";
7 | import { s3Upload } from "../libs/awsLib";
8 | import config from "../config";
9 | import "./Notes.css";
10 |
11 | export default function Notes() {
12 | const file = useRef(null);
13 | const { id } = useParams();
14 | const history = useHistory();
15 | const [note, setNote] = useState(null);
16 | const [content, setContent] = useState("");
17 | const [isLoading, setIsLoading] = useState(false);
18 | const [isDeleting, setIsDeleting] = useState(false);
19 |
20 | useEffect(() => {
21 | function loadNote() {
22 | return API.get("notes", `/notes/${id}`);
23 | }
24 |
25 | async function onLoad() {
26 | try {
27 | const note = await loadNote();
28 | const { content, attachment } = note;
29 |
30 | if (attachment) {
31 | note.attachmentURL = await Storage.vault.get(attachment);
32 | }
33 |
34 | setContent(content);
35 | setNote(note);
36 | } catch (e) {
37 | onError(e);
38 | }
39 | }
40 |
41 | onLoad();
42 | }, [id]);
43 |
44 | function validateForm() {
45 | return content.length > 0;
46 | }
47 |
48 | function formatFilename(str) {
49 | return str.replace(/^\w+-/, "");
50 | }
51 |
52 | function handleFileChange(event) {
53 | file.current = event.target.files[0];
54 | }
55 |
56 | function saveNote(note) {
57 | return API.put("notes", `/notes/${id}`, {
58 | body: note
59 | });
60 | }
61 |
62 | async function handleSubmit(event) {
63 | let attachment;
64 |
65 | event.preventDefault();
66 |
67 | if (file.current && file.current.size > config.MAX_ATTACHMENT_SIZE) {
68 | alert(
69 | `Please pick a file smaller than ${
70 | config.MAX_ATTACHMENT_SIZE / 1000000
71 | } MB.`
72 | );
73 | return;
74 | }
75 |
76 | setIsLoading(true);
77 |
78 | try {
79 | if (file.current) {
80 | attachment = await s3Upload(file.current);
81 | }
82 |
83 | await saveNote({
84 | content,
85 | attachment: attachment || note.attachment
86 | });
87 | history.push("/");
88 | } catch (e) {
89 | onError(e);
90 | setIsLoading(false);
91 | }
92 | }
93 |
94 | function deleteNote() {
95 | return API.del("notes", `/notes/${id}`);
96 | }
97 |
98 | async function handleDelete(event) {
99 | event.preventDefault();
100 |
101 | const confirmed = window.confirm(
102 | "Are you sure you want to delete this note?"
103 | );
104 |
105 | if (!confirmed) {
106 | return;
107 | }
108 |
109 | setIsDeleting(true);
110 |
111 | try {
112 | await deleteNote();
113 | history.push("/");
114 | } catch (e) {
115 | onError(e);
116 | setIsDeleting(false);
117 | }
118 | }
119 |
120 | return (
121 |
122 | {note && (
123 |
169 | )}
170 |
171 | );
172 | }
173 |
--------------------------------------------------------------------------------
/src/containers/ResetPassword.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 480px) {
2 | .ResetPassword {
3 | padding: 60px 0;
4 | }
5 |
6 | .ResetPassword form {
7 | margin: 0 auto;
8 | max-width: 320px;
9 | }
10 |
11 | .ResetPassword .success {
12 | max-width: 400px;
13 | }
14 | }
15 |
16 | .ResetPassword .success {
17 | margin: 0 auto;
18 | text-align: center;
19 | }
20 | .ResetPassword .success .glyphicon {
21 | color: grey;
22 | font-size: 30px;
23 | margin-bottom: 30px;
24 | }
25 |
--------------------------------------------------------------------------------
/src/containers/ResetPassword.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Auth } from "aws-amplify";
3 | import { Link } from "react-router-dom";
4 | import {
5 | HelpBlock,
6 | FormGroup,
7 | Glyphicon,
8 | FormControl,
9 | ControlLabel,
10 | } from "react-bootstrap";
11 | import LoaderButton from "../components/LoaderButton";
12 | import { useFormFields } from "../libs/hooksLib";
13 | import { onError } from "../libs/errorLib";
14 | import "./ResetPassword.css";
15 |
16 | export default function ResetPassword() {
17 | const [fields, handleFieldChange] = useFormFields({
18 | code: "",
19 | email: "",
20 | password: "",
21 | confirmPassword: "",
22 | });
23 | const [codeSent, setCodeSent] = useState(false);
24 | const [confirmed, setConfirmed] = useState(false);
25 | const [isConfirming, setIsConfirming] = useState(false);
26 | const [isSendingCode, setIsSendingCode] = useState(false);
27 |
28 | function validateCodeForm() {
29 | return fields.email.length > 0;
30 | }
31 |
32 | function validateResetForm() {
33 | return (
34 | fields.code.length > 0 &&
35 | fields.password.length > 0 &&
36 | fields.password === fields.confirmPassword
37 | );
38 | }
39 |
40 | async function handleSendCodeClick(event) {
41 | event.preventDefault();
42 |
43 | setIsSendingCode(true);
44 |
45 | try {
46 | await Auth.forgotPassword(fields.email);
47 | setCodeSent(true);
48 | } catch (error) {
49 | onError(error);
50 | setIsSendingCode(false);
51 | }
52 | }
53 |
54 | async function handleConfirmClick(event) {
55 | event.preventDefault();
56 |
57 | setIsConfirming(true);
58 |
59 | try {
60 | await Auth.forgotPasswordSubmit(
61 | fields.email,
62 | fields.code,
63 | fields.password
64 | );
65 | setConfirmed(true);
66 | } catch (error) {
67 | onError(error);
68 | setIsConfirming(false);
69 | }
70 | }
71 |
72 | function renderRequestCodeForm() {
73 | return (
74 |
94 | );
95 | }
96 |
97 | function renderConfirmationForm() {
98 | return (
99 |
139 | );
140 | }
141 |
142 | function renderSuccessMessage() {
143 | return (
144 |
145 |
146 |
Your password has been reset.
147 |
148 |
149 | Click here to login with your new credentials.
150 |
151 |
152 |
153 | );
154 | }
155 |
156 | return (
157 |
158 | {!codeSent
159 | ? renderRequestCodeForm()
160 | : !confirmed
161 | ? renderConfirmationForm()
162 | : renderSuccessMessage()}
163 |
164 | );
165 | }
166 |
--------------------------------------------------------------------------------
/src/containers/Settings.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 480px) {
2 | .Settings {
3 | padding: 60px 0;
4 | margin: 0 auto;
5 | max-width: 480px;
6 | }
7 |
8 | .Settings > .LoaderButton:first-child {
9 | margin-bottom: 15px;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/containers/Settings.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { API } from "aws-amplify";
3 | import { useHistory } from "react-router-dom";
4 | import { Elements, StripeProvider } from "react-stripe-elements";
5 | import { LinkContainer } from "react-router-bootstrap";
6 | import LoaderButton from "../components/LoaderButton";
7 | import BillingForm from "../components/BillingForm";
8 | import { onError } from "../libs/errorLib";
9 | import config from "../config";
10 | import "./Settings.css";
11 |
12 | export default function Settings() {
13 | const history = useHistory();
14 | const [stripe, setStripe] = useState(null);
15 | const [isLoading, setIsLoading] = useState(false);
16 |
17 | useEffect(() => {
18 | setStripe(window.Stripe(config.STRIPE_KEY));
19 | }, []);
20 |
21 | function billUser(details) {
22 | return API.post("notes", "/billing", {
23 | body: details
24 | });
25 | }
26 |
27 | async function handleFormSubmit(storage, { token, error }) {
28 | if (error) {
29 | onError(error);
30 | return;
31 | }
32 |
33 | setIsLoading(true);
34 |
35 | try {
36 | await billUser({
37 | storage,
38 | source: token.id
39 | });
40 |
41 | alert("Your card has been charged successfully!");
42 | history.push("/");
43 | } catch (e) {
44 | onError(e);
45 | setIsLoading(false);
46 | }
47 | }
48 |
49 | return (
50 |
51 |
52 |
53 | Change Email
54 |
55 |
56 |
57 |
58 | Change Password
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/containers/Signup.css:
--------------------------------------------------------------------------------
1 | @media all and (min-width: 480px) {
2 | .Signup {
3 | padding: 60px 0;
4 | }
5 |
6 | .Signup form {
7 | margin: 0 auto;
8 | max-width: 320px;
9 | }
10 | }
11 |
12 | .Signup form span.help-block {
13 | font-size: 14px;
14 | padding-bottom: 10px;
15 | color: #999;
16 | }
17 |
--------------------------------------------------------------------------------
/src/containers/Signup.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { Auth } from "aws-amplify";
3 | import { useHistory } from "react-router-dom";
4 | import {
5 | HelpBlock,
6 | FormGroup,
7 | FormControl,
8 | ControlLabel
9 | } from "react-bootstrap";
10 | import LoaderButton from "../components/LoaderButton";
11 | import { useAppContext } from "../libs/contextLib";
12 | import { useFormFields } from "../libs/hooksLib";
13 | import { onError } from "../libs/errorLib";
14 | import "./Signup.css";
15 |
16 | export default function Signup() {
17 | const [fields, handleFieldChange] = useFormFields({
18 | email: "",
19 | password: "",
20 | confirmPassword: "",
21 | confirmationCode: "",
22 | });
23 | const history = useHistory();
24 | const [newUser, setNewUser] = useState(null);
25 | const { userHasAuthenticated } = useAppContext();
26 | const [isLoading, setIsLoading] = useState(false);
27 |
28 | function validateForm() {
29 | return (
30 | fields.email.length > 0 &&
31 | fields.password.length > 0 &&
32 | fields.password === fields.confirmPassword
33 | );
34 | }
35 |
36 | function validateConfirmationForm() {
37 | return fields.confirmationCode.length > 0;
38 | }
39 |
40 | async function handleSubmit(event) {
41 | event.preventDefault();
42 |
43 | setIsLoading(true);
44 |
45 | try {
46 | const newUser = await Auth.signUp({
47 | username: fields.email,
48 | password: fields.password,
49 | });
50 | setIsLoading(false);
51 | setNewUser(newUser);
52 | } catch (e) {
53 | onError(e);
54 | setIsLoading(false);
55 | }
56 | }
57 |
58 | async function handleConfirmationSubmit(event) {
59 | event.preventDefault();
60 |
61 | setIsLoading(true);
62 |
63 | try {
64 | await Auth.confirmSignUp(fields.email, fields.confirmationCode);
65 | await Auth.signIn(fields.email, fields.password);
66 |
67 | userHasAuthenticated(true);
68 | history.push("/");
69 | } catch (e) {
70 | onError(e);
71 | setIsLoading(false);
72 | }
73 | }
74 |
75 | function renderConfirmationForm() {
76 | return (
77 |
98 | );
99 | }
100 |
101 | function renderForm() {
102 | return (
103 |
139 | );
140 | }
141 |
142 | return (
143 |
144 | {newUser === null ? renderForm() : renderConfirmationForm()}
145 |
146 | );
147 | }
148 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | padding: 0;
4 | font-family: "Open Sans", sans-serif;
5 | font-size: 16px;
6 | color: #333;
7 | -webkit-font-smoothing: antialiased;
8 | -moz-osx-font-smoothing: grayscale;
9 | }
10 | h1, h2, h3, h4, h5, h6 {
11 | font-family: "PT Serif", serif;
12 | }
13 |
14 | h1, h2, h3, h4, h5, h6 {
15 | font-family: "PT Serif", serif;
16 | }
17 |
18 | select.form-control,
19 | textarea.form-control,
20 | input.form-control {
21 | font-size: 16px;
22 | }
23 | input[type=file] {
24 | width: 100%;
25 | }
26 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import { Amplify } from 'aws-amplify';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 | import './index.css';
6 | import App from './App';
7 | import config from './config';
8 | import { initSentry } from './libs/errorLib';
9 | import * as serviceWorker from './serviceWorker';
10 |
11 | initSentry();
12 |
13 | Amplify.configure({
14 | Auth: {
15 | mandatorySignIn: true,
16 | region: config.cognito.REGION,
17 | userPoolId: config.cognito.USER_POOL_ID,
18 | identityPoolId: config.cognito.IDENTITY_POOL_ID,
19 | userPoolWebClientId: config.cognito.APP_CLIENT_ID
20 | },
21 | Storage: {
22 | region: config.s3.REGION,
23 | bucket: config.s3.BUCKET,
24 | identityPoolId: config.cognito.IDENTITY_POOL_ID
25 | },
26 | API: {
27 | endpoints: [
28 | {
29 | name: "notes",
30 | endpoint: config.apiGateway.URL,
31 | region: config.apiGateway.REGION
32 | },
33 | ]
34 | }
35 | });
36 |
37 | ReactDOM.render(
38 |
39 |
40 | ,
41 | document.getElementById('root')
42 | );
43 |
44 | // If you want your app to work offline and load faster, you can change
45 | // unregister() to register() below. Note this comes with some pitfalls.
46 | // Learn more about service workers: https://bit.ly/CRA-PWA
47 | serviceWorker.unregister();
48 |
--------------------------------------------------------------------------------
/src/libs/awsLib.js:
--------------------------------------------------------------------------------
1 | import { Storage } from "aws-amplify";
2 |
3 | export async function s3Upload(file) {
4 | const filename = `${Date.now()}-${file.name}`;
5 |
6 | const stored = await Storage.vault.put(filename, file, {
7 | contentType: file.type,
8 | });
9 |
10 | return stored.key;
11 | }
12 |
--------------------------------------------------------------------------------
/src/libs/contextLib.js:
--------------------------------------------------------------------------------
1 | import { useContext, createContext } from "react";
2 |
3 | export const AppContext = createContext(null);
4 |
5 | export function useAppContext() {
6 | return useContext(AppContext);
7 | }
8 |
--------------------------------------------------------------------------------
/src/libs/errorLib.js:
--------------------------------------------------------------------------------
1 | import * as Sentry from "@sentry/browser";
2 |
3 | const isLocal = process.env.NODE_ENV === "development";
4 |
5 | export function initSentry() {
6 | if (isLocal) {
7 | return;
8 | }
9 |
10 | Sentry.init({ dsn: "https://5f83aa2e21064e47bab8a1f308f940eb@sentry.io/5185720" });
11 | }
12 |
13 | export function logError(error, errorInfo = null) {
14 | if (isLocal) {
15 | return;
16 | }
17 |
18 | Sentry.withScope((scope) => {
19 | errorInfo && scope.setExtras(errorInfo);
20 | Sentry.captureException(error);
21 | });
22 | }
23 |
24 | export function onError(error) {
25 | let errorInfo = {};
26 | let message = error.toString();
27 |
28 | // Auth errors
29 | if (!(error instanceof Error) && error.message) {
30 | errorInfo = error;
31 | message = error.message;
32 | error = new Error(message);
33 | // API errors
34 | } else if (error.config && error.config.url) {
35 | errorInfo.url = error.config.url;
36 | }
37 |
38 | logError(error, errorInfo);
39 |
40 | alert(message);
41 | }
42 |
--------------------------------------------------------------------------------
/src/libs/hooksLib.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | export function useFormFields(initialState) {
4 | const [fields, setValues] = useState(initialState);
5 |
6 | return [
7 | fields,
8 | function(event) {
9 | setValues({
10 | ...fields,
11 | [event.target.id]: event.target.value
12 | });
13 | }
14 | ];
15 | }
16 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------