initial serverless fullstack setup
This commit is contained in:
58
site/src/App.js
Normal file
58
site/src/App.js
Normal 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
333
site/src/App.module.css
Normal 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
18
site/src/config.js
Normal 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
|
||||
32
site/src/fragments/Loading/Loading.js
Normal file
32
site/src/fragments/Loading/Loading.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
47
site/src/fragments/Loading/Loading.module.css
Normal file
47
site/src/fragments/Loading/Loading.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
1
site/src/fragments/Loading/index.js
Normal file
1
site/src/fragments/Loading/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Loading'
|
||||
123
site/src/index.css
Normal file
123
site/src/index.css
Normal 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
15
site/src/index.js
Normal 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
249
site/src/pages/Auth/Auth.js
Normal 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)
|
||||
231
site/src/pages/Auth/Auth.module.css
Normal file
231
site/src/pages/Auth/Auth.module.css
Normal 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;
|
||||
}
|
||||
82
site/src/pages/Dashboard/Dashboard.js
Normal file
82
site/src/pages/Dashboard/Dashboard.js
Normal 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)
|
||||
92
site/src/pages/Dashboard/Dashboard.module.css
Normal file
92
site/src/pages/Dashboard/Dashboard.module.css
Normal 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;
|
||||
}
|
||||
64
site/src/pages/Home/Home.js
Normal file
64
site/src/pages/Home/Home.js
Normal 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)
|
||||
105
site/src/pages/Home/Home.module.css
Normal file
105
site/src/pages/Home/Home.module.css
Normal 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
71
site/src/utils/api.js
Normal 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
76
site/src/utils/helpers.js
Normal 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
2
site/src/utils/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './helpers'
|
||||
export * from './api'
|
||||
Reference in New Issue
Block a user