initial serverless fullstack setup

This commit is contained in:
2020-10-29 19:05:45 -05:00
commit 6275da68cd
45 changed files with 2377 additions and 0 deletions

68
site/README.md Normal file
View File

@@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `yarn build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

37
site/package.json Normal file
View File

@@ -0,0 +1,37 @@
{
"name": "serverless-fullstack-app-website",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.3",
"js-cookie": "^2.2.1",
"moment": "^2.24.0",
"react-router-dom": "^5.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
site/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

27
site/public/index.html Normal file
View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link
rel="shortcut icon"
href="%PUBLIC_URL%/favicon.ico"
/>
<link href="https://fonts.googleapis.com/css2?family=Titillium+Web:wght@300;400;600;700&display=swap" rel="stylesheet"> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="A ready-to-use serverless fullstack application template. Built with AWS Lambda, Express.js, React, AWS HTTP API, AWS DynamoDB and more!"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Serverless Fullstack Application</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

25
site/public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "Serverless Fullstack Application",
"name": "Serverless Fullstack Application",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
site/public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

11
site/serverless.yml Normal file
View File

@@ -0,0 +1,11 @@
component: website
name: site
inputs:
# React application. "hook" runs before deployment to build the source code. "dist" is the built artifact directory which is uploaded.
src:
src: ./
hook: npm run build
dist: build
# Enable this when you want to set a custom domain.
# domain: ${env:domain}

58
site/src/App.js Normal file
View File

@@ -0,0 +1,58 @@
import React, { Component } from 'react'
import {
BrowserRouter as Router,
Switch,
Route,
} from 'react-router-dom'
import Home from './pages/Home/Home'
import Auth from './pages/Auth/Auth'
import Dashboard from './pages/Dashboard/Dashboard'
import { getSession } from './utils'
export default class App extends Component {
constructor(props) {
super(props)
this.state = {}
}
async componentDidMount() {
// console.log(getSession())
}
render() {
return (
<Router>
<Switch>
<Route path='/register'>
<Auth />
</Route>
<Route path='/login'>
<Auth />
</Route>
<PrivateRoute
exact
path='/'
component={Dashboard}
/>
</Switch>
</Router>
)
}
}
/**
* A component to protect routes.
* Shows Auth page if the user is not authenticated
*/
const PrivateRoute = ({ component, ...options }) => {
const session = getSession()
const finalComponent = session ? Dashboard : Home
return <Route {...options} component={finalComponent} />
}

333
site/src/App.module.css Normal file
View File

@@ -0,0 +1,333 @@
.container {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
box-sizing: border-box;
overflow: hidden;
width: 100%;
height: 100vh;
}
.containerInner {
display: flex;
flex-direction: column;
box-sizing: border-box;
background: #000;
width: 100%;
height: 100%;
align-self: center;
max-width: 700px;
padding: 50px 15px 30px 15px;
}
.containerLoading {
display: flex;
flex-direction: row;
width: 100%;
height: auto;
justify-content: center;
align-content: center;
padding: 160px 0 0 0;
}
.column {
display: flex;
flex-direction: row;
box-sizing: border-box;
height: 100%;
width: 50%;
}
.row {
display: flex;
flex-direction: column;
box-sizing: border-box;
width: 100%;
}
.heroArtwork {
display: flex;
flex-direction: row;
box-sizing: border-box;
align-content: center;
justify-content: center;
width: 100%;
}
.heroArtwork img {
width: auto;
height: auto;
align-self: center;
max-height: 100%;
max-width: 500px;
user-select: none;
}
.heroTitle {
display: flex;
flex-direction: row;
box-sizing: border-box;
align-content: center;
justify-content: center;
width: 100%;
}
.heroTitle img {
width: auto;
height: auto;
align-self: center;
max-height: 100%;
max-width: 500px;
user-select: none;
}
.heroDescription {
display: flex;
text-align: center;
font-size: 18px;
line-height: 30px;
margin: 10px 0 32px 0px;
}
.error {
display: flex;
flex-direction: column;
width: 100%;
margin: 18px 0;
align-content: center;
justify-content: center;
text-align: center;
color: #FD5750;
font-size: 18px;
}
.success {
display: flex;
flex-direction: column;
width: 100%;
margin: 32px 0;
align-content: center;
justify-content: center;
text-align: center;
text-transform: lowercase;
color: #78f542;
font-size: 22px;
}
.containerRegister {}
.containerSignIn {}
.socialButtons {
display: flex;
flex-direction: row;
box-sizing: border-box;
margin: 15px auto 0px auto;
width: 100%;
}
.buttonGithub {
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
box-sizing: border-box;
margin: 0 auto 10px auto;
width: 100%;
padding: 12px;
border-radius: 4px;
font-size: 19px;
font-weight: 500;
opacity: 0.6;
transition: all 0.3s ease;
text-transform: lowercase;
}
.buttonGithub:hover {
cursor: pointer;
opacity: 1;
}
.buttonGithub img {
max-height: 28px;
margin-right: 13px;
margin-top: 1.5px;
}
.buttonGoogle {
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
box-sizing: border-box;
width: 100%;
padding: 12px;
border-radius: 4px;
font-size: 19px;
font-weight: 500;
opacity: 0.6;
transition: all 0.3s ease;
text-transform: lowercase;
}
.buttonGoogle:hover {
cursor: pointer;
opacity: 1;
}
.buttonGoogle img {
max-height: 25px;
margin-right: 11px;
margin-top: 3px;
}
.formType {
display: flex;
flex-direction: row;
margin: 0px auto 0px auto;
width: 100%;
padding: 10px 0;
font-size: 22px;
color: rgba(255,255,255,0.6);
text-transform: lowercase;
user-select: none;
}
.formTypeRegister {
display: flex;
width: 50%;
padding: 5px 40px 5px 0;
justify-content: flex-end;
border-right: 2px solid rgba(255,255,255,0.15);
}
.formTypeRegister:hover {
cursor: pointer;
}
.formTypeSignIn {
display: flex;
width: 50%;
padding: 5px 0 5px 40px;
text-align: left;
}
.formTypeSignIn:hover {
cursor: pointer;
}
.formTypeActive {
color: rgba(255,255,255,1);
}
.form {
display: flex;
flex-direction: column;
max-width: 380px;
width: 100%;
margin: 10px auto 0 auto;
text-align: center;
}
.formField {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 25px;
}
.formLabel {
font-size: 19px;
font-weight: 500;
margin: 0 0 10px 0;
user-select: none;
display: none;
}
.formInput {
text-align: center;
font-size: 22px;
padding: 10px 0px 22px 0px;
border-bottom: 1.5px solid rgba(255,255,255,0.4);
border-top: 1.5px solid transparent;
border-right: 1.5px solid transparent;
border-left: 1.5px solid transparent;
transition: all 0.6s ease;
background: none;
outline: none;
color: #ffffff;
caret-color: white;
}
.formInput::placeholder {
text-align: center;
font-size: 22px;
color: rgba(255,255,255,0.5);
margin: 0;
}
.formInput:focus {
border-bottom: 1.5px solid rgba(255,255,255,1);
}
.formError {
margin: 10px 0 10px 0;
color: #FD5750;
font-size: 18px;
line-height: 32px;
}
.formButton {
margin: 16px auto 0 auto!important;
}
.formButton:hover {
cursor: pointer;
transform: scale(1.05);
}
.formButton:active {
cursor: pointer;
transform: scale(1);
}
.forgotPassword {
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
margin: 20px auto 5px auto;
width: 100%;
padding: 10px 0;
font-size: 19px;
color: rgba(255,255,255,0.6);
transition: all 0.3s ease;
}
.forgotPassword:hover {
cursor: pointer;
color: rgba(255,255,255,1);
}
.githubLink {
display: flex;
justify-content: center;
align-content: center;
width: 100%;
margin: 26px 0 40px 0;
}
.githubLink a {
font-size: 20px;
color: #FD5750;
opacity: 0.7;
transition: all 0.3s ease;
text-decoration: none;
}
.githubLink:hover a {
cursor: pointer;
opacity: 1;
}

18
site/src/config.js Normal file
View File

@@ -0,0 +1,18 @@
/**
* Global Config
*/
const config = {}
// Domains
config.domains = {}
/**
* API Domain
* Add the domain from your serverless express.js back-end here.
* This will enable your front-end to communicate with your back-end.
* (e.g. 'https://api.mydomain.com' or 'https://091jafsl10.execute-api.us-east-1.amazonaws.com')
*/
config.domains.api = null
export default config

View File

@@ -0,0 +1,32 @@
import React, { Component } from 'react'
import styles from './Loading.module.css'
export default class Loading extends Component {
constructor(props) {
super(props)
this.state = {}
}
async componentDidMount() {}
render() {
return (
<div className={`${this.props.className}`}>
<div className={`${styles.container}`}>
<img
draggable={false}
alt={`Loading`}
src={
'https://s3.amazonaws.com/dashboard.serverless.com/images/icon-serverless-framework.png'
}
/>
<p>loading...</p>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,47 @@
.container {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
box-sizing: border-box;
width: auto;
height: 100px;
opacity: 0.45;
text-align: center;
}
.container img {
display: flex;
align-self: center;
height: 100%;
width: auto;
max-width: 100px;
max-height: 100px;
margin: 0 0 20px 0;
animation-duration: 0.4s;
animation-name: pulse;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease;
filter: none;
-webkit-filter: grayscale(100%);
-moz-filter: grayscale(100%);
-ms-filter: grayscale(100%);
-o-filter: grayscale(100%);
}
.container p {
font-size: 22px;
color: rgba(255,255,255,0.5);
}
@keyframes pulse {
from {
transform: scale(1);
}
to {
transform: scale(1.15);
}
}

View File

@@ -0,0 +1 @@
export { default } from './Loading'

123
site/src/index.css Normal file
View File

@@ -0,0 +1,123 @@
html, body, #root {
display: block;
width: 100%;
height: 100%;
min-height: 100vh;
box-sizing: border-box;
background: #000;
color: #ffffff;
margin: 0;
font-family: 'Titillium Web', sans-serif;
font-weight: 500;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 18px;
}
div, h1, h2, h3, h4, h5, h6, p, span, input, button, li {
font-family: 'Titillium Web', sans-serif;
font-size: 24px;
color: #ffffff;
}
/**
* Links
*/
a, .link {
font-family: 'Titillium Web', sans-serif;
font-size: 24px;
font-weight: 400;
color: rgba(255,255,255,0.6);
text-decoration: none;
transition: all 0.14s ease;
}
a:hover, .link:hover {
cursor: pointer;
color: rgba(255,255,255,1);
}
a:active, .link:active {
color: rgba(255,255,255,0.6);
}
/**
* Buttons
*/
.buttonPrimaryLarge {
text-align: center;
font-size: 18px;
width: 100%;
max-width: 360px;
height: auto;
margin: 20px 0 0 0;
padding: 24px 24px 28px 24px;
background: #fd5750;
color: #fff;
font-family: 'Titillium Web', sans-serif;
font-size: 24px;
font-weight: 600;
border-radius: 4px;
border: 0px;
transition: all 0.14s ease;
user-select: none;
outline: none;
text-transform: lowercase;
}
.buttonPrimaryLarge:hover {
cursor: pointer;
transform: scale(1.03);
}
.buttonPrimaryLarge:active {
cursor: pointer;
transform: scale(1);
outline: none;
}
/**
* Animations
*/
.animateFadeIn {
animation: fadeIn 0.45s;
}
.animateScaleIn {
animation: scaleIn 0.35s;
}
.animateFlicker {
animation: flicker 2.5s infinite;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes flicker {
0% {
opacity: 0.3;
}
50% {
opacity: 1;
}
100% {
opacity: 0.3;
}
}

15
site/src/index.js Normal file
View File

@@ -0,0 +1,15 @@
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import './index.css'
/**
* Render App
*/
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
)

249
site/src/pages/Auth/Auth.js Normal file
View File

@@ -0,0 +1,249 @@
import React, { Component } from 'react'
import {
Link,
withRouter,
} from 'react-router-dom'
import Loading from '../../fragments/Loading'
import styles from './Auth.module.css'
import {
userRegister,
userLogin,
userGet,
saveSession,
} from '../../utils'
class Auth extends Component {
constructor(props) {
super(props)
const pathName = window.location.pathname.replace('/', '')
this.state = {}
this.state.state = pathName
this.state.loading = true
this.state.error = null
this.state.formEmail = ''
this.state.formPassword = ''
// Bindings
this.handleFormInput = this.handleFormInput.bind(this)
this.handleFormSubmit = this.handleFormSubmit.bind(this)
this.handleFormTypeChange = this.handleFormTypeChange.bind(this)
}
/**
* Component did mount
*/
componentDidMount() {
this.setState({
loading: false
})
// Clear query params
const url = document.location.href
window.history.pushState({}, '', url.split('?')[0])
}
/**
* Handles a form change
*/
handleFormTypeChange(type) {
this.setState({ state: type },
() => {
this.props.history.push(`/${type}`)
})
}
/**
* Handle text changes within form fields
*/
handleFormInput(field, value) {
value = value.trim()
const nextState = {}
nextState[field] = value
this.setState(Object.assign(this.state, nextState))
}
/**
* Handles form submission
* @param {object} evt
*/
async handleFormSubmit(evt) {
evt.preventDefault()
this.setState({ loading: true })
// Validate email
if (!this.state.formEmail) {
return this.setState({
loading: false,
formError: 'email is required'
})
}
// Validate password
if (!this.state.formPassword) {
return this.setState({
loading: false,
formError: 'password is required'
})
}
let token
try {
if (this.state.state === 'register') {
token = await userRegister(this.state.formEmail, this.state.formPassword)
} else {
token = await userLogin(this.state.formEmail, this.state.formPassword)
}
} catch (error) {
console.log(error)
if (error.message) {
this.setState({
formError: error.message,
loading: false
})
} else {
this.setState({
formError: 'Sorry, something unknown went wrong. Please try again.',
loading: false
})
}
return
}
// Fetch user record and set session in cookie
let user = await userGet(token.token)
user = user.user
saveSession(user.id, user.email, token.token)
window.location.replace('/')
}
render() {
return (
<div className={`${styles.container} animateFadeIn`}>
<div className={styles.containerInner}>
{ /* Logo */}
<Link to='/' className={`${styles.logo}`}>
<img
draggable='false'
src={'./fullstack-app-title.png'}
alt='serverless-fullstack-application'
/>
</Link>
{ /* Loading */}
{this.state.loading && (
<div>
{< Loading className={styles.containerLoading} />}
</div>
)}
{ /* Registration Form */}
{!this.state.loading && (
<div className={styles.formType}>
<div
className={
`${styles.formTypeRegister}
${this.state.state === 'register' ? styles.formTypeActive : ''}`}
onClick={(e) => { this.handleFormTypeChange('register') }}>
Register
</div>
<div
className={
`${styles.formTypeSignIn}
${this.state.state === 'login' ? styles.formTypeActive : ''}`}
onClick={(e) => { this.handleFormTypeChange('login') }}>
Sign-In
</div>
</div>
)}
{this.state.state === 'register' && !this.state.loading && (
<div className={styles.containerRegister}>
<form className={styles.form} onSubmit={this.handleFormSubmit}>
<div className={styles.formField}>
<label className={styles.formLabel}>email</label>
<input
type='text'
placeholder='yours@example.com'
className={styles.formInput}
value={this.state.formEmail}
onChange={(e) => { this.handleFormInput('formEmail', e.target.value) }}
/>
</div>
<div className={styles.formField}>
<label className={styles.formLabel}>password</label>
<input
type='password'
placeholder='your password'
className={styles.formInput}
value={this.state.formPassword}
onChange={(e) => { this.handleFormInput('formPassword', e.target.value) }}
/>
</div>
{this.state.formError && (
<div className={styles.formError}>{this.state.formError}</div>
)}
<input
className={`buttonPrimaryLarge ${styles.formButton}`}
type='submit'
value='Register'
/>
</form>
</div>
)}
{this.state.state === 'login' && !this.state.loading && (
<div className={styles.containerSignIn}>
<form className={styles.form} onSubmit={this.handleFormSubmit}>
<div className={styles.formField}>
<label className={styles.formLabel}>email</label>
<input
type='text'
placeholder='yours@example.com'
className={styles.formInput}
value={this.state.formEmail}
onChange={(e) => { this.handleFormInput('formEmail', e.target.value) }}
/>
</div>
<div className={styles.formField}>
<label className={styles.formLabel}>password</label>
<input
type='password'
placeholder='your password'
className={styles.formInput}
value={this.state.formPassword}
onChange={(e) => { this.handleFormInput('formPassword', e.target.value) }}
/>
</div>
{this.state.formError && (
<div className={styles.formError}>{this.state.formError}</div>
)}
<input className={`buttonPrimaryLarge ${styles.formButton}`} type='submit' value='Sign In' />
</form>
</div>
)}
</div>
</div>
)
}
}
export default withRouter(Auth)

View File

@@ -0,0 +1,231 @@
.container {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
box-sizing: border-box;
overflow: hidden;
width: 100%;
height: 100vh;
}
.containerInner {
display: flex;
flex-direction: column;
box-sizing: border-box;
background: #000;
width: 100%;
height: 100%;
align-self: center;
max-width: 700px;
padding: 50px 15px 30px 15px;
}
.containerLoading {
display: flex;
flex-direction: row;
width: 100%;
height: auto;
justify-content: center;
align-content: center;
padding: 160px 0 0 0;
}
.logo {
display: flex;
flex-direction: row;
box-sizing: border-box;
align-content: center;
justify-content: center;
width: 100%;
}
.logo img {
width: auto;
height: auto;
align-self: center;
max-height: 100%;
max-width: 400px;
user-select: none;
}
.error {
display: flex;
flex-direction: column;
width: 100%;
margin: 18px 0;
align-content: center;
justify-content: center;
text-align: center;
color: #FD5750;
font-size: 18px;
}
.success {
display: flex;
flex-direction: column;
width: 100%;
margin: 32px 0;
align-content: center;
justify-content: center;
text-align: center;
text-transform: lowercase;
color: #78f542;
font-size: 22px;
}
.containerRegister {}
.containerSignIn {}
.formType {
display: flex;
flex-direction: row;
margin: 26px auto 0px auto;
width: 100%;
padding: 10px 0;
font-size: 22px;
color: rgba(255,255,255,0.6);
text-transform: lowercase;
user-select: none;
}
.formTypeRegister {
display: flex;
width: 50%;
padding: 5px 40px 5px 0;
justify-content: flex-end;
border-right: 2px solid rgba(255,255,255,0.15);
color: rgba(255,255,255,0.6);
}
.formTypeRegister:hover {
cursor: pointer;
}
.formTypeSignIn {
display: flex;
width: 50%;
padding: 5px 0 5px 40px;
text-align: left;
color: rgba(255,255,255,0.6);
}
.formTypeSignIn:hover {
cursor: pointer;
}
.formTypeActive {
color: rgba(255,255,255,1);
}
.form {
display: flex;
flex-direction: column;
max-width: 380px;
width: 100%;
margin: 10px auto 0 auto;
text-align: center;
}
.formField {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 25px;
}
.formLabel {
font-size: 19px;
font-weight: 500;
margin: 0 0 10px 0;
user-select: none;
display: none;
}
.formInput {
text-align: center;
font-size: 22px;
padding: 10px 0px 22px 0px;
border-bottom: 1.5px solid rgba(255,255,255,0.4);
border-top: 1.5px solid transparent;
border-right: 1.5px solid transparent;
border-left: 1.5px solid transparent;
transition: all 0.6s ease;
background: none;
outline: none;
color: #ffffff;
caret-color: white;
}
.formInput::placeholder {
text-align: center;
font-size: 22px;
color: rgba(255,255,255,0.5);
margin: 0;
}
.formInput:focus {
border-bottom: 1.5px solid rgba(255,255,255,1);
}
.formError {
margin: 10px 0 10px 0;
color: #FD5750;
font-size: 18px;
line-height: 32px;
}
.formButton {
margin: 16px auto 0 auto!important;
}
.formButton:hover {
cursor: pointer;
transform: scale(1.05);
}
.formButton:active {
cursor: pointer;
transform: scale(1);
}
.forgotPassword {
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
margin: 20px auto 5px auto;
width: 100%;
padding: 10px 0;
font-size: 19px;
color: rgba(255,255,255,0.6);
transition: all 0.3s ease;
}
.forgotPassword:hover {
cursor: pointer;
color: rgba(255,255,255,1);
}
.githubLink {
display: flex;
justify-content: center;
align-content: center;
width: 100%;
margin: 26px 0 40px 0;
}
.githubLink a {
font-size: 20px;
color: #FD5750;
opacity: 0.7;
transition: all 0.3s ease;
text-decoration: none;
}
.githubLink:hover a {
cursor: pointer;
opacity: 1;
}

View File

@@ -0,0 +1,82 @@
import React, { Component } from 'react'
import {
withRouter
} from 'react-router-dom'
import styles from './Dashboard.module.css'
import {
getSession,
deleteSession
} from '../../utils'
class Dashboard extends Component {
constructor(props) {
super(props)
this.state = {}
// Bindings
this.logout = this.logout.bind(this)
}
async componentDidMount() {
const userSession = getSession()
this.setState({
session: userSession,
})
}
/**
* Log user out by clearing cookie and redirecting
*/
logout() {
deleteSession()
this.props.history.push(`/`)
}
render() {
return (
<div className={`${styles.container} animateFadeIn`}>
<div className={styles.containerInner}>
{ /* Navigation */ }
<div className={styles.navigationContainer}>
<div
className={`link`}>
{ this.state.session ? this.state.session.userEmail : '' }
</div>
<div
className={`link`}
onClick={this.logout}>
logout
</div>
</div>
{ /* Content */ }
<div className={`${styles.contentContainer}`}>
<div className={`${styles.artwork} animateFlicker`}>
<img
draggable='false'
src={'./fullstack-app-artwork.png'}
alt='serverless-fullstack-application'
/>
</div>
<div className={`${styles.welcomeMessage}`}>
Welcome to your serverless fullstack dashboard...
</div>
</div>
</div>
</div>
)
}
}
export default withRouter(Dashboard)

View File

@@ -0,0 +1,92 @@
.container {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
box-sizing: border-box;
overflow: hidden;
width: 100%;
height: 100vh;
}
.containerInner {
display: flex;
flex-direction: column;
box-sizing: border-box;
background: #000;
width: 100%;
height: 100%;
padding: 0 0 30px 0;
}
.navigationContainer {
display: flex;
flex-direction: row;
justify-content: flex-end;
align-content: center;
box-sizing: border-box;
background: #000;
width: 100%;
height: 80px;
padding: 25px 45px 0 45px;
}
.navigationContainer div {
margin-left: 40px;
}
.contentContainer {
display: flex;
flex-direction: column;
align-content: center;
box-sizing: border-box;
background: #000;
width: 100%;
height: 100%;
padding: 7% 15px 45px 15px;
}
.welcomeMessage {
display: flex;
flex-direction: column;
align-content: center;
text-align: center;
width: 100%;
height: auto;
font-size: 28px;
}
.artwork {
display: flex;
flex-direction: row;
box-sizing: border-box;
align-content: center;
justify-content: center;
width: 100%;
height: 250px;
}
.artwork img {
width: auto;
height: auto;
align-self: center;
max-height: 100%;
max-width: 500px;
user-select: none;
}
/**
* Special
*/
::-moz-selection {
background: #fd5750;
color: #ffffff;
opacity: 1;
}
::selection {
background: #fd5750;
color: #ffffff;
opacity: 1;
}

View File

@@ -0,0 +1,64 @@
import React, { Component } from 'react'
import {
Link,
withRouter
} from 'react-router-dom'
import styles from './Home.module.css'
class Home extends Component {
constructor(props) {
super(props)
this.state = {}
}
async componentDidMount() { }
render() {
return (
<div className={`${styles.container} animateFadeIn`}>
<div className={styles.containerInner}>
{ /* Hero Artwork */}
<div className={`${styles.heroArtwork} animateFlicker`}>
<img
draggable='false'
src={'./fullstack-app-artwork.png'}
alt='serverless-fullstack-application'
/>
</div>
<div className={`${styles.heroTitle}`}>
<img
draggable='false'
src={'./fullstack-app-title.png'}
alt='serverless-fullstack-application'
/>
</div>
{ /* Hero Description */}
<div className={`${styles.heroDescription}`}>
A serverless full-stack application built with AWS Lambda, AWS HTTP API, Express.js, React & AWS DynamoDB.
</div>
{ /* Call To Action */}
<div className={`${styles.containerCta}`}>
<Link to='/register'>
<button className={`buttonPrimaryLarge`}>
Register
</button>
</Link>
<Link to='/login' className={`${styles.linkSignIn}`}>sign-in</Link>
</div>
</div>
</div>
)
}
}
export default withRouter(Home)

View File

@@ -0,0 +1,105 @@
.container {
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
box-sizing: border-box;
overflow: hidden;
width: 100%;
height: 100vh;
}
.containerInner {
display: flex;
flex-direction: column;
box-sizing: border-box;
background: #000;
width: 100%;
height: 100%;
align-self: center;
max-width: 700px;
padding: 50px 15px 30px 15px;
}
.heroArtwork {
display: flex;
flex-direction: row;
box-sizing: border-box;
align-content: center;
justify-content: center;
width: 100%;
}
.heroArtwork img {
width: auto;
height: auto;
align-self: center;
max-height: 100%;
max-width: 500px;
user-select: none;
}
.heroTitle {
display: flex;
flex-direction: row;
box-sizing: border-box;
align-content: center;
justify-content: center;
width: 100%;
}
.heroTitle img {
width: auto;
height: auto;
align-self: center;
max-height: 100%;
max-width: 500px;
user-select: none;
}
.heroDescription {
display: flex;
text-align: center;
font-weight: 400;
font-size: 24px;
line-height: 38px;
margin: 32px 0px 52px 0;
}
.containerCta {
display: flex;
flex-direction: column;
justify-content: center;
align-content: center;
text-align: center;
}
.containerCta button {
margin: 0px auto 20px auto;
}
.containerCta .linkSignIn {
padding: 10px;
color: #fd5750;
opacity: 0.65;
}
.containerCta .linkSignIn:hover {
opacity: 1;
}
/**
* Special
*/
::-moz-selection {
background: #fd5750;
color: #ffffff;
opacity: 1;
}
::selection {
background: #fd5750;
color: #ffffff;
opacity: 1;
}

71
site/src/utils/api.js Normal file
View File

@@ -0,0 +1,71 @@
/**
* Utils: Back-end
*/
import config from '../config'
/**
* Register a new user
*/
export const userRegister = async (email, password) => {
return await requestApi('/users/register', 'POST', { email, password })
}
/**
* Login a new user
*/
export const userLogin = async (email, password) => {
return await requestApi('/users/login', 'POST', { email, password })
}
/**
* userGet
*/
export const userGet = async (token) => {
return await requestApi('/user', 'POST', null, {
Authorization: `Bearer ${token}`
})
}
/**
* API request to call the backend
*/
export const requestApi = async (
path = '',
method = 'GET',
data = null,
headers = {}) => {
// Check if API URL has been set
if (!config?.domains?.api) {
throw new Error(`Error: Missing API Domain Please add the API domain from your serverless Express.js back-end to this front-end application. You can do this in the "site" folder, in the "./config.js" file. Instructions are listed there and in the documentation.`)
}
// Prepare URL
if (!path.startsWith('/')) {
path = `/${path}`
}
const url = `${config.domains.api}${path}`
// Set headers
headers = Object.assign(
{ 'Content-Type': 'application/json' },
headers
)
// Default options are marked with *
const response = await fetch(url, {
method: method.toUpperCase(),
mode: 'cors',
cache: 'no-cache',
headers,
body: data ? JSON.stringify(data) : null
})
if (response.status < 200 || response.status >= 300) {
const error = await response.json()
throw new Error(error.error)
}
return await response.json()
}

76
site/src/utils/helpers.js Normal file
View File

@@ -0,0 +1,76 @@
import Cookies from 'js-cookie'
/**
* Format Org and Username correctly for the Serverless Platform backend
*/
export const formatOrgAndUsername = (name = '') => {
name = name.toString().toLowerCase().replace(/[^a-z\d-]+/gi, '-')
// Remove multiple instances of hyphens
name = name.replace(/-{2,}/g, '-')
if (name.length > 40) {
name = name.substring(0, 40)
}
return name
}
/**
* Parse query parameters in a URL
* @param {*} searchString
*/
export const parseQueryParams = (searchString = null) => {
if (!searchString) {
return null
}
// Clone string
let clonedParams = (' ' + searchString).slice(1)
return clonedParams
.substr(1)
.split('&')
.filter((el) => el.length)
.map((el) => el.split('='))
.reduce(
(accumulator, currentValue) =>
Object.assign(accumulator, {
[decodeURIComponent(currentValue.shift())]: decodeURIComponent(currentValue.pop())
}),
{}
)
}
/**
* Parse hash fragment parameters in a URL
*/
export const parseHashFragment = (hashString) => {
const hashData = {}
let hash = decodeURI(hashString)
hash = hash.split('&')
hash.forEach((val) => {
val = val.replace('#', '')
hashData[val.split('=')[0]] = val.split('=')[1]
})
return hashData
}
/**
* Save session in browser cookie
*/
export const saveSession = (userId, userEmail, userToken) => {
Cookies.set('serverless', { userId, userEmail, userToken })
}
/**
* Get session in browser cookie
*/
export const getSession = () => {
const data = Cookies.get('serverless')
return data ? JSON.parse(data) : null
}
/**
* Delete session in browser cookie
*/
export const deleteSession = () => {
Cookies.remove('serverless')
}

2
site/src/utils/index.js Normal file
View File

@@ -0,0 +1,2 @@
export * from './helpers'
export * from './api'