Merge pull request #9 from EbookFoundation/feature/login-ui

feature/login ui
pull/10/head^2
Theodore Kluge 2018-10-29 19:57:19 -04:00 committed by GitHub
commit df4c964d12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1076 additions and 256 deletions

11
.babelrc Normal file
View File

@ -0,0 +1,11 @@
{
"presets": [
[
"@babel/preset-env", {
"useBuiltIns": "usage"
}
],
"@babel/preset-react"
],
"plugins": ["@babel/plugin-proposal-object-rest-spread"],
}

114
.gitignore vendored
View File

@ -1,119 +1,7 @@
################################################
# ┌─┐┬┌┬┐╦╔═╗╔╗╔╔═╗╦═╗╔═╗
# │ ┬│ │ ║║ ╦║║║║ ║╠╦╝║╣
# o└─┘┴ ┴ ╩╚═╝╝╚╝╚═╝╩╚═╚═╝
#
# > Files to exclude from your app's repo.
#
# This file (`.gitignore`) is only relevant if
# you are using git.
#
# It exists to signify to git that certain files
# and/or directories should be ignored for the
# purposes of version control.
#
# This keeps tmp files and sensitive credentials
# from being uploaded to your repository. And
# it allows you to configure your app for your
# machine without accidentally committing settings
# which will smash the local settings of other
# developers on your team.
#
# Some reasonable defaults are included below,
# but, of course, you should modify/extend/prune
# to fit your needs!
#
################################################
################################################
# Local Configuration
#
# Explicitly ignore files which contain:
#
# 1. Sensitive information you'd rather not push to
# your git repository.
# e.g., your personal API keys or passwords.
#
# 2. Developer-specific configuration
# Basically, anything that would be annoying
# to have to change every time you do a
# `git pull` on your laptop.
# e.g. your local development database, or
# the S3 bucket you're using for file uploads
# during development.
#
################################################
config/local.js
################################################
# Dependencies
#
#
# When releasing a production app, you _could_
# hypothetically include your node_modules folder
# in your git repo, but during development, it
# is always best to exclude it, since different
# developers may be working on different kernels,
# where dependencies would need to be recompiled
# anyway.
#
# Most of the time, the node_modules folder can
# be excluded from your code repository, even
# in production, thanks to features like the
# package-lock.json file / NPM shrinkwrap.
#
# But no matter what, since this is a Sails app,
# you should always push up the package-lock.json
# or shrinkwrap file to your repository, to avoid
# accidentally pulling in upgraded dependencies
# and breaking your code.
#
# That said, if you are having trouble with
# dependencies, (particularly when using
# `npm link`) this can be pretty discouraging.
# But rather than just adding the lockfile to
# your .gitignore, try this first:
# ```
# rm -rf node_modules
# rm package-lock.json
# npm install
# ```
#
# [?] For more tips/advice, come by and say hi
# over at https://sailsjs.com/support
#
################################################
node_modules
################################################
#
# > Do you use bower?
# > re: the bower_components dir, see this:
# > http://addyosmani.com/blog/checking-in-front-end-dependencies/
# > (credit Addy Osmani, @addyosmani)
#
################################################
################################################
# Temporary files generated by Sails/Waterline.
################################################
.tmp
################################################
# Miscellaneous
#
# Common files generated by text editors,
# operating systems, file systems, dbs, etc.
################################################
*~
*#
.DS_STORE
@ -123,7 +11,7 @@ nbproject
.node_history
dump.rdb
npm-debug.log
npm-debug.log.*
lib-cov
*.seed
*.log

View File

@ -5,5 +5,8 @@
"_generatedWith": {
"sails": "1.0.2",
"sails-generate": "1.15.28"
},
"hooks": {
"grunt": false
}
}

View File

@ -0,0 +1,86 @@
'use strict'
import Ajax from '../lib/Ajax'
const ACTIONS = {
set_working: 'set_working',
set_user: 'set_user',
set_password: 'set_password',
set_carousel: 'set_carousel',
set_error: 'set_error',
clear_error: 'clear_error'
}
export default ACTIONS
export const setWorking = working => ({
type: ACTIONS.set_working,
data: working
})
export const setEmail = email => ({
type: ACTIONS.set_user,
data: email
})
export const setPassword = pass => ({
type: ACTIONS.set_password,
data: pass
})
export const setCarousel = pos => ({
type: ACTIONS.set_carousel,
data: pos
})
export const setError = data => ({
type: ACTIONS.set_error,
data: data
})
export const clearError = () => ({
type: ACTIONS.clear_error
})
export const setLoggedIn = (data) => (dispatch, getState) => {
document.localStorage.setItem('roe-token', JSON.stringify(data))
window.location.href = '/app'
}
export const checkEmail = email => (dispatch, getState) => {
// dispatch(setWorking(true))
dispatch(clearError())
if (/^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/.test(email)) {
dispatch(setCarousel(2))
} else {
dispatch(setError({
type: 'email',
error: 'Please enter a valid email address.'
}))
}
// dispatch(setWorking(false))
}
export const checkPassword = (email, password) => async (dispatch, getState) => {
dispatch(setWorking(true))
// do email + password check
try {
const res = await Ajax.post({
url: '/api/token',
data: {
grant_type: 'credentials',
email,
password
}
})
dispatch(setLoggedIn(res))
// dispatch(setWorking(false))
} catch (e) {
dispatch(setError({
type: 'password',
error: e.toString()
}))
dispatch(setWorking(false))
}
}

View File

@ -0,0 +1,11 @@
'use strict'
import React from 'react'
const Progress = props => (
<div className={'progress' + (props.bound ? ' bound' : '')}>
<div className='indeterminate' />
</div>
)
export default Progress

View File

@ -0,0 +1,23 @@
'use strict'
import React from 'react'
import STYLE from '../../styles/shared/underlineinput.scss'
const UnderlineInput = props => (
<div className='underlined-input'>
<input
type={props.type}
name={props.name}
value={props.value}
className={(props.value.length ? 'has-content' : '')}
autoComplete='nothing'
onChange={props.onChange} />
<div className='reacts-to'>
<label className='placeholder'>{props.placeholder}</label>
<div className='underline' />
</div>
</div>
)
export default UnderlineInput

View File

@ -0,0 +1,58 @@
'use strict'
import React from 'react'
import STYLE from '../../styles/shared/carousel.scss'
class Carousel extends React.Component {
constructor () {
super()
this.getWidth = this.getWidth.bind(this)
this.getOffset = this.getOffset.bind(this)
}
getWidth () {
return this.props.children.length * 450
}
getOffset () {
return -this.props.position * 450
}
render () {
return (
<section className='carousel-container'>
<div className='carousel' style={{width: this.getWidth(), left: this.getOffset()}}>
{this.props.children}
</div>
</section>
)
}
}
function handleClick (e, fn) {
e.preventDefault()
fn(e)
}
const CarouselItem = props => (
<form className='carousel-item' onSubmit={(e) => handleClick(e, props.onButtonClick)}>
<header className='modal-header'>
<h1>{props.header}</h1>
{props.headerExtraContent}
</header>
{props.inputs}
<span className='carousel-error'>{props.error}</span>
<div className='button-row'>
<a href='#' onClick={props.onSmallButtonClick}>{props.smallButton}</a>
<button className='btn btn-primary' type='submit' >
{props.button}
</button>
</div>
{props.footer &&
<footer className='footer-row'>
<a href='#'>{props.footer}</a>
</footer>}
</form>
)
export default Carousel
export {
CarouselItem
}

162
assets/js/lib/Ajax.js Normal file
View File

@ -0,0 +1,162 @@
/* global XMLHttpRequest FormData */
'use strict'
let ajaxcfg = {}
class AjaxError extends Error {
constructor (reason, data, xhr) {
super(reason)
this.data = data
this.xhr = xhr
}
}
export default class Ajax {
static async get (opts) {
if (!opts) opts = {}
opts.method = 'get'
return Ajax.ajax(opts)
}
static async post (opts) {
if (!opts) opts = {}
opts.method = 'post'
return Ajax.ajax(opts)
}
static async put (opts) {
if (!opts) opts = {}
opts.method = 'put'
return Ajax.ajax(opts)
}
static async patch (opts) {
if (!opts) opts = {}
opts.method = 'patch'
return Ajax.ajax(opts)
}
static async delete (opts) {
if (!opts) opts = {}
opts.method = 'delete'
return Ajax.ajax(opts)
}
static async head (opts) {
if (!opts) opts = {}
opts.method = 'head'
return Ajax.ajax(opts)
}
static async options (opts) {
if (!opts) opts = {}
opts.method = 'options'
return Ajax.ajax(opts)
}
static ajax (opts) {
return new Promise((resolve, reject) => {
if (!opts) reject(new Error('Missing required options parameter.'))
if (opts.method) {
if (!['get', 'post', 'put', 'patch', 'delete', 'head', 'options'].includes(opts.method.toLowerCase())) reject(new Error('opts.method must be one of: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.'))
opts.method = opts.method.toUpperCase()
}
var xhr = opts.xhr || new XMLHttpRequest()
var fd = null
var qs = ''
if (opts.data && opts.method.toLowerCase() !== 'get') {
fd = new FormData()
for (let key in opts.data) {
fd.append(key, opts.data[key])
}
} else if (opts.data) {
qs += '?'
let params = []
for (let key in opts.data) {
params.push([key, opts.data[key]].join('='))
}
qs += params.join('&')
}
xhr.onload = () => {
if (xhr.status !== 200) return xhr.onerror()
var data = xhr.response
resolve({
data,
xhr
})
}
xhr.onerror = () => {
var data = xhr.response
// method not allowed
if (xhr.status === 405) {
reject(new AjaxError('405 Method Not Allowed', data, xhr))
return
} else if (xhr.status === 404) {
reject(new AjaxError('404 Not Found', data, xhr))
return
}
try {
// if the access token is invalid, try to use the refresh token
var json = JSON.parse(data)
if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('invalid') && ajaxcfg.refresh_token) {
return Ajax.refresh(opts)
} else if (json.error === 'access_denied' && json.hint.includes('token') && json.hint.includes('revoked')) {
reject(new AjaxError('token revoked', data, xhr))
}
} catch (e) {
reject(new AjaxError(e.toString(), data, xhr))
} finally {
reject(new AjaxError(xhr.status, data, xhr))
}
}
xhr.open(opts.method || 'GET', opts.url + qs || window.location.href)
if (opts.headers) {
for (let key in opts.headers) xhr.setRequestHeader(key, opts.headers[key])
}
if (ajaxcfg.access_token && !(opts.headers || {}).Authorization) xhr.setRequestHeader('Authorization', 'Bearer ' + ajaxcfg.access_token)
xhr.send(fd)
})
}
static refresh (opts) {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest()
var fd = new FormData()
const OAUTH_TOKEN_REQUEST = {
grant_type: 'refresh_token',
refresh_token: ajaxcfg.refresh_token,
client_id: 'foxfile',
client_secret: 1
}
for (var key in OAUTH_TOKEN_REQUEST) {
fd.append(key, OAUTH_TOKEN_REQUEST[key])
}
// try original request
xhr.onload = () => {
if (xhr.status !== 200) return xhr.onerror()
if (ajaxcfg.refresh) ajaxcfg.refresh(xhr.response)
var json = JSON.parse(xhr.response)
ajaxcfg.access_token = json.access_token
ajaxcfg.refresh_token = json.refresh_token
return Ajax.ajax(opts)
}
// if this fails, dont try again
xhr.onerror = () => {
var data = xhr.response
reject(new AjaxError(xhr.status, data, xhr))
}
xhr.open('POST', ajaxcfg.refresh_url)
xhr.send(fd)
})
}
static setTokenData (tokens) {
if (!tokens) throw new Error('Missing tokens.')
if (!tokens.access_token && !tokens.refresh_token && !tokens.refresh_url) throw new Error('Missing at least one of: access_token, refresh_token, refresh_url.')
if (tokens.access_token) ajaxcfg.access_token = tokens.access_token
if (tokens.refresh_token) ajaxcfg.refresh_token = tokens.refresh_token
if (tokens.refresh_url) ajaxcfg.refresh_url = tokens.refresh_url
return true
}
static onRefresh (func) {
ajaxcfg.refresh = func
}
}

127
assets/js/login.js Normal file
View File

@ -0,0 +1,127 @@
'use strict'
import React from 'react'
import ReactDOM from 'react-dom'
import Progress from './components/Progress'
import Carousel, {CarouselItem} from './containers/Carousel'
import UnderlineInput from './components/UnderlineInput'
import reducer from './reducers/login'
import {setEmail, setPassword, setCarousel, checkEmail, checkPassword} from './actions/login'
import STYLE from '../styles/login.scss'
class App extends React.Component {
constructor () {
super()
this.state = {
carouselPosition: 1,
emailError: '',
passwordError: '',
user: {
email: '',
password: ''
},
working: false
}
this.dispatch = this.dispatch.bind(this)
this.getEmailInputs = this.getEmailInputs.bind(this)
this.getPasswordInputs = this.getPasswordInputs.bind(this)
this.getPasswordHeader = this.getPasswordHeader.bind(this)
}
dispatch (action) {
if (!action) throw new Error('dispatch: missing action')
if (action instanceof Function) {
action(this.dispatch, () => this.state)
} else {
const changes = reducer(this.state, action)
if (!changes || !Object.keys(changes).length) return
this.setState({
...changes
})
}
}
getEmailInputs () {
return [
<UnderlineInput
key={0}
type='text'
name='email'
placeholder='Email'
onChange={e => this.dispatch(setEmail(e.target.value))}
value={this.state.user.email} />
]
}
getPasswordInputs () {
return [
<UnderlineInput
key={0}
type='password'
name='password'
placeholder='Password'
onChange={e => this.dispatch(setPassword(e.target.value))}
value={this.state.user.password} />
]
}
getPasswordHeader () {
return (
<section className='account-banner flex-container'>
<div className='stack flex'>
<span className='email'>{this.state.user.email}</span>
</div>
<a href='#' onClick={() => this.dispatch(setCarousel(1))}>Not you?</a>
</section>
)
}
render () {
return (
<div className='root-container flex-container flex-center'>
<section className={'window' + (this.state.working ? ' working' : '')}>
<Progress bound />
<Carousel position={this.state.carouselPosition} >
<CarouselItem
header='Sign up'
inputs={this.getEmailInputs()}
error={this.state.emailError}
button='Sign up'
onButtonClick={() => null}
smallButton='Have an account?'
onSmallButtonClick={() => this.dispatch(setCarousel(1))}
footer='Sign up with your Google account' />
<CarouselItem
header='Sign in'
inputs={this.getEmailInputs()}
error={this.state.emailError}
button='Next'
onButtonClick={() => this.dispatch(checkEmail(this.state.user.email))}
smallButton='Create account'
onSmallButtonClick={() => this.dispatch(setCarousel(0))}
footer='Sign in with your Google account' />
<CarouselItem
header='Welcome'
headerExtraContent={this.getPasswordHeader()}
inputs={this.getPasswordInputs()}
error={this.state.passwordError}
button='Sign in'
onButtonClick={() => this.dispatch(checkPassword(this.state.user.email, this.state.user.password))}
smallButton='Forgot password?'
onSmallButtonClick={() => this.dispatch(setCarousel(3))} />
<CarouselItem
header='Password recovery'
inputs={this.getEmailInputs()}
error={this.state.emailError}
button='Send recovery email'
onButtonClick={() => null}
smallButton='Log in'
onSmallButtonClick={() => this.dispatch(setCarousel(1))} />
</Carousel>
</section>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))

View File

@ -0,0 +1,50 @@
'use strict'
import Actions from '../actions/login'
const reducer = (state = {}, action) => {
const {type, data} = action
switch (type) {
case Actions.set_user:
return {
user: {
...state.user,
email: data
}
}
case Actions.set_password:
return {
user: {
...state.user,
password: data
}
}
case Actions.set_carousel:
return {
carouselPosition: data
}
case Actions.set_working:
return {
working: data
}
case Actions.set_error:
switch (data.type) {
case 'email':
return {
emailError: data.error
}
case 'password':
return {
passwordError: data.error
}
default: return {}
}
case Actions.clear_error:
return {
emailError: '',
passwordError: ''
}
}
}
export default reducer

View File

@ -1,24 +0,0 @@
/**
* importer.less
*
* By default, new Sails projects are configured to compile this file
* from LESS to CSS. Unlike CSS files, LESS files are not compiled and
* included automatically unless they are imported below.
*
* For more information see:
* https://sailsjs.com/anatomy/assets/styles/importer-less
*/
// For example:
//
// @import 'variables/colors.less';
// @import 'mixins/foo.less';
// @import 'mixins/bar.less';
// @import 'mixins/baz.less';
//
// @import 'styleguide.less';
// @import 'pages/login.less';
// @import 'pages/signup.less';
//
// etc.

View File

@ -0,0 +1,141 @@
@import 'vars';
html,
body,
#root,
.root-container {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
background: $background-1;
font-family: sans-serif;
}
* {
box-sizing: border-box;
}
.flex-container {
display: flex;
&.flex-center {
align-items: center;
justify-content: center;
flex-direction: column;
}
&.flex-horizintal {
flex-direction: row;
}
&.flex-vertical {
flex-direction: column;
}
.flex {
flex: 1;
}
}
.btn {
position: relative;
height: 36px;
padding: 0 20px;
background: $accent-2;
color: white;
border: none;
border-radius: 3px;
outline: none;
font-size: 0.9rem;
text-transform: uppercase;
box-shadow: $shadow-1;
transition: box-shadow .15s ease-in-out;
cursor: pointer;
user-select: none;
&:focus{
outline: initial;
}
&:hover {
box-shadow: $shadow-3;
}
&:active {
box-shadow: $shadow-2;
}
&.btn-with-icon {
padding: 0 20px 0 0;
i {
font-size: 1.5em;
height: 30px;
width: 30px;
vertical-align: middle;
margin: 0 10px;
}
}
&.btn-social-signin {
text-transform: none;
&.btn-google {
background: #dd4b39;
}
}
&.btn-clear {
background: transparent;
color: $text-dark-1;
box-shadow: none;
border: 1px solid $accent-2;
&:hover {
box-shadow: $shadow-2;
}
}
& + .btn {
margin-left: 6px;
}
}
a:hover {
text-decoration: underline !important;
}
@-webkit-keyframes indeterminate {0% {left: -35%; right: 100%; } 60% {left: 100%; right: -90%; } 100% {left: 100%; right: -90%; } } @keyframes indeterminate {0% {left: -35%; right: 100%; } 60% {left: 100%; right: -90%; } 100% {left: 100%; right: -90%; } } @-webkit-keyframes indeterminate-short {0% {left: -200%; right: 100%; } 60% {left: 107%; right: -8%; } 100% {left: 107%; right: -8%; } } @keyframes indeterminate-short {0% {left: -200%; right: 100%; } 60% {left: 107%; right: -8%; } 100% {left: 107%; right: -8%; } }
.progress {
position: absolute;
display: inline-block;
top: 0;
height: 0;
width: 100%;
background-color: $accent-3;
background-clip: padding-box;
margin: 0;
overflow: hidden;
transition: height .2s $transition;
.indeterminate {
background-color: $accent-2;
&:before {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
animation: indeterminate 2.1s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite;
}
&:after {
content: '';
position: absolute;
background-color: inherit;
top: 0;
left: 0;
bottom: 0;
will-change: left, right;
-webkit-animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
animation: indeterminate-short 2.1s cubic-bezier(0.165, 0.84, 0.44, 1) infinite;
-webkit-animation-delay: 1.15s;
animation-delay: 1.15s;
}
}
}

View File

@ -0,0 +1,29 @@
$shadow-0: 0 2px 2px 0 rgba(0,0,0,0.14), 0 3px 1px -2px rgba(0,0,0,0.12), 0 1px 5px 0 rgba(0,0,0,0.2);
$shadow-1: 0 1.5px 4px rgba(0, 0, 0, 0.24), 0 1.5px 6px rgba(0, 0, 0, 0.12);
$shadow-2: 0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);
$shadow-3: 0 8px 17px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
$shadow-4: 0 10px 20px rgba(0, 0, 0, 0.22), 0 14px 56px rgba(0, 0, 0, 0.25);
$shadow-5: 0 15px 24px rgba(0, 0, 0, 0.22), 0 19px 76px rgba(0, 0, 0, 0.3);
$transition: cubic-bezier(0.23, 0.54, 0.19, 0.99);
$transition-2: cubic-bezier(0.08, 0.54, 0.45, 0.91);
$black-1: rgba(0,0,0,.87);
$black-2: rgba(0,0,0,.54);
$black-3: rgba(0,0,0,.38);
$black-4: rgba(0,0,0,.12);
$black-5: rgba(0,0,0,.07);
$auth-width: 450px;
$background-1: #f2f2f2;
$background-2: white;
$text-dark-1: $black-1;
$text-dark-2: $black-2;
$accent-1: #4423c4;
$accent-2: #4460c4;
$accent-3: #D4DBF1;
$red: #FE4C52;

2
assets/styles/login.scss Normal file
View File

@ -0,0 +1,2 @@
@import 'lib/default';
@import 'shared/auth';

View File

@ -0,0 +1,50 @@
@import '../lib/vars';
#root:after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 45%;
background: $accent-1;
box-shadow: $shadow-0;
z-index: 0;
}
.window {
position: relative;
height: 500px;
width: $auth-width;
background: $background-2;
box-shadow: $shadow-1;
z-index: 1;
padding: 40px 0;
overflow: hidden;
border-radius: 3px;
&:before {
opacity: 0;
z-index: 2;
position: absolute;
content: '';
height: 100%;
width: 100%;
top: 4px;
left: 0;
background: rgba(255,255,255,.50);
transition: .2s opacity $transition;
pointer-events: none;
}
&.working {
& > .progress {
top: 0;
height: 4px;
}
&:before {
opacity: 1;
pointer-events: initial;
}
}
}

View File

@ -0,0 +1,127 @@
@import '../lib/vars';
.carousel-container {
position: relative;
height: 100%;
width: 100%;
overflow-x: hidden;
padding: 0 0 20px 0;
opacity: 1;
transition: opacity .2s $transition;
.carousel {
position: absolute;
height: 100%;
display: flex;
transition: left 0.4s $transition;
.carousel-item {
// width: $auth-width;
flex: 1;
height: 100%;
display: inline-block;
// float: left;
margin: 0 40px;
position: relative;
header {
height: 128px;
h1 {
font-weight: normal;
font-size: 1.5rem;
margin: 0;
}
.account-banner {
height: 50px;
margin-top: 10px;
line-height: 50px;
display: flex;
img {
height: 50px;
width: 50px;
border-radius: 50%;
vertical-align: middle;
}
.info-stack {
flex: 1;
padding: 0 10px;
height: 100%;
line-height: 16px;
.name {
display: inline-block;
color: $text-dark-1;
width: 100%;
}
.email {
display: inline-block;
color: $text-dark-2;
width: 100%;
}
.hidden {
display: none;
}
}
a {
color: $accent-2;
text-decoration: none;
}
p {
line-height: initial;
}
}
}
.carousel-error {
color: $red;
display: inline-block;
font-size: 0.9rem;
}
footer {
text-align: right;
position: absolute;
bottom: 20px;
width: 100%;
margin: 0;
color: $text-dark-2;
.btn {
margin-left: 20px;
}
a {
color: $accent-2;
text-decoration: none;
}
}
.button-row {
height: 50px;
line-height: 50px;
width: 100%;
text-align: right;
a {
color: $accent-2;
text-decoration: none;
float: left;
}
}
&.working {
pointer-events: none;
.input-carousel {
opacity: 0.5;
}
.progress {
height: 4px;
}
}
@media screen and (max-width: 600px) {
& {
width: 100%;
height: 100%;
box-shadow: none;
}
}
}
}
}

View File

@ -0,0 +1,87 @@
@import '../lib/vars';
.underlined-input,
.underlined-input-readonly {
width: 100%;
height: 46px;
padding: 14px 0 0 0;
margin: 0 0 8px 0;
line-height: 26px;
position: relative;
$transition-time: .3s;
input {
position: absolute;
background: transparent;
border: none;
outline: none;
height: 26px;
width: 100%;
font-size: 1rem;
bottom: 0;
& + .reacts-to {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 46px;
pointer-events: none;
label {
position: absolute;
top: 20px;
font-size: 0.95rem;
color: $black-2;
pointer-events: none;
transition: all $transition-time $transition;
}
.underline {
position: absolute;
bottom: 0;
width: 100%;
height: 1px;
background: $black-4;
&:before {
content: '';
position: absolute;
left: 50%;
top: 0;
width: 0;
height: 2px;
background: $accent-2;
transition: left $transition-time $transition,
width $transition-time $transition;
}
}
}
&:focus + .reacts-to,
&:active + .reacts-to,
&.has-content + .reacts-to {
label {
top: 0;
font-size: 0.7rem;
line-height: 14px;
color: $accent-2;
}
.underline:before {
width: 100%;
left: 0;
}
}
&.invalid:focus + .reacts-to,
&.invalid:active + .reacts-to,
&.invalid.has-content + .reacts-to {
label {
color: $red;
}
}
&.invalid + .reacts-to {
.underline {
background: $red;
}
}
}
}

View File

@ -0,0 +1,16 @@
<% var key, item %>
<% htmlWebpackPlugin.options.links = htmlWebpackPlugin.options.links || [] %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>RoE - Login</title>
<% for (item of htmlWebpackPlugin.options.links) {
if (typeof item === 'string' || item instanceof String) { item = { href: item, rel: 'stylesheet' } } %>
<link<% for (key in item) { %> <%= key %>="<%= item[key] %>"<% } %> /><%
} %>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@ -64,7 +64,5 @@ module.exports.http = {
// var middlewareFn = skipper({ strict: true });
// return middlewareFn;
// })(),
},
};
}
}

View File

@ -23,7 +23,13 @@ module.exports.routes = {
***************************************************************************/
'/': {
view: 'pages/homepage'
view: 'pages/index'
},
'/login': {
view: 'pages/login'
},
'/register': {
view: 'pages/login'
},
/***************************************************************************
@ -37,7 +43,6 @@ module.exports.routes = {
* *
***************************************************************************/
// ╔═╗╔═╗╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗
// ╠═╣╠═╝║ ║╣ ║║║ ║║╠═╝║ ║║║║║ ║ ╚═╗
// ╩ ╩╩ ╩ ╚═╝╝╚╝═╩╝╩ ╚═╝╩╝╚╝ ╩ ╚═╝

View File

@ -12,20 +12,48 @@
"express-rate-limit": "^3.2.1",
"forever": "^0.15.3",
"grunt": "^1.0.3",
"react": "^16.6.0",
"react-dom": "^16.6.0",
"sails": "^1.0.2",
"sails-hook-grunt": "^3.0.2",
"sails-hook-orm": "^2.1.1",
"sails-hook-sockets": "^1.4.0"
},
"devDependencies": {
"@sailshq/eslint": "^4.19.3"
"@babel/core": "^7.1.2",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"@sailshq/eslint": "^4.19.3",
"babel-loader": "^8.0.4",
"css-loader": "^1.0.1",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.4",
"node-sass": "^4.9.4",
"npm-run-all": "^4.1.3",
"rimraf": "^2.6.2",
"sass-loader": "^7.1.0",
"style-loader": "^0.23.1",
"webpack": "^4.23.1",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10"
},
"scripts": {
"start": "sudo NODE_ENV='production' ./node_modules/.bin/forever start app.js",
"start": "npm run open:client",
"start:debug": "npm-run-all --parallel open:client debug",
"start:prod": "npm-run-all --parallel build:prod lift",
"open:client": "webpack-dev-server --mode development",
"build": "npm run build:prod",
"build:dev": "webpack --mode development",
"build:prod": "webpack --mode production",
"clean": "rimraf .tmp && mkdirp .tmp/public",
"lift": "sails lift",
"forever": "sudo NODE_ENV='production' ./node_modules/.bin/forever start app.js",
"stop": "./node_modules/.bin/forever stopall",
"test": "npm run lint && npm run custom-tests && echo 'Done.'",
"lint": "eslint . --max-warnings=0 --report-unused-disable-directives && echo '✔ Your .js files look good.'",
"custom-tests": "echo \"(No other custom tests yet.)\" && echo"
"debug": "node --inspect app.js"
},
"main": "app.js",
"repository": {

View File

@ -1,110 +1 @@
<!DOCTYPE html>
<html>
<head>
<title>New Sails App</title>
<!-- Viewport mobile tag for sensible mobile support -->
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<!--
Stylesheets and Preprocessors
==============================
You can always bring in CSS files manually with `<link>` tags, or asynchronously
using a solution like AMD (RequireJS). Or, if you like, you can take advantage
of Sails' conventional asset pipeline (boilerplate Gruntfile).
By default, stylesheets from your `assets/styles` folder are included
here automatically (between STYLES and STYLES END). Both CSS (.css) and LESS (.less)
are supported. In production, your styles will be minified and concatenated into
a single file.
To customize any part of the built-in behavior, just edit `tasks/pipeline.js`.
For example, here are a few things you could do:
+ Change the order of your CSS files
+ Import stylesheets from other directories
+ Use a different or additional preprocessor, like SASS, SCSS or Stylus
-->
<!--STYLES-->
<link rel="stylesheet" href="/styles/importer.css">
<!--STYLES END-->
</head>
<body>
<%- body %>
<!--
Client-side Templates
========================
HTML templates are important prerequisites of modern, rich client applications.
To work their magic, frameworks like React, Vue.js, Angular, Ember, and Backbone
require that you load these templates client-side.
By default, your Gruntfile is configured to automatically load and precompile
client-side JST templates in your `assets/templates` folder, then
include them here automatically (between TEMPLATES and TEMPLATES END).
To customize this behavior to fit your needs, just edit `tasks/pipeline.js`.
For example, here are a few things you could do:
+ Import templates from other directories
+ Use a different view engine (handlebars, dust, pug/jade, etc.)
+ Internationalize your client-side templates using a server-side
stringfile before they're served.
-->
<!--TEMPLATES-->
<!--TEMPLATES END-->
<!--
Server-side View Locals
========================
Sometimes, it's convenient to get access to your server-side view locals from
client-side JavaScript. This can improve page load times, remove the need for
extra AJAX requests, and make your client-side code easier to understand and
to maintain. Sails provides a simple mechanism for accessing dynamic view
locals: the "exposeLocalsToBrowser()" view partial.
For more information on using this built-in feature, see:
https://sailsjs.com/docs/concepts/views/locals#?escaping-untrusted-data-using-exposelocalstobrowser
-->
<!--
Client-side Javascript
========================
You can always bring in JS files manually with `script` tags, or asynchronously
on the client using a solution like AMD (RequireJS). Or, if you like, you can
take advantage of Sails' conventional asset pipeline (boilerplate Gruntfile).
By default, files in your `assets/js` folder are included here
automatically (between SCRIPTS and SCRIPTS END). Both JavaScript (.js) and
CoffeeScript (.coffee) are supported. In production, your scripts will be minified
and concatenated into a single file.
To customize any part of the built-in behavior, just edit `tasks/pipeline.js`.
For example, here are a few things you could do:
+ Change the order of your scripts
+ Import scripts from other directories
+ Use a different preprocessor, like TypeScript
-->
<!--SCRIPTS-->
<script src="/dependencies/sails.io.js"></script>
<!--SCRIPTS END-->
</body>
</html>
<%- body %>

View File

@ -19,6 +19,7 @@ setTimeout(function sunrise () {
<div class="header">
<h1 id="main-title" class="container"><%= __('A brand new app.') %></h1>
<h3 class="container">You're looking at: <code><%= view.pathFromApp + '.' +view.ext %></code></h3>
<h3>Go to Login: <a href="/login">Here</a></h3>
</div>
<div class="main container clearfix">
<ul class="getting-started">

1
views/pages/login.ejs Normal file
View File

@ -0,0 +1 @@
<%- partial('../../.tmp/public/login.html') %>

49
webpack.config.js Normal file
View File

@ -0,0 +1,49 @@
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const webpack = require('webpack')
const path = require('path')
module.exports = (env, argv) => {
const mode = argv.mode || 'development'
return {
mode: mode || 'development',
entry: {
login: './assets/js/login.js'
},
output: {
path: path.join(__dirname, '/.tmp/public'),
filename: '[name].bundle.js'
},
module: {
rules: [
{
use: 'babel-loader',
test: /\.jsx?$/,
exclude: /node_modules/
},
{
test: /\.scss$/,
use: [
mode !== 'production' ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: 'assets/templates/login.html',
links: mode === 'production' ? [{ rel: 'stylesheet', type: 'text/css', href: 'login.css' }] : [],
filename: path.join(__dirname, '/.tmp/public/login.html')
}),
new MiniCssExtractPlugin({
filename: '[name].css'
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(mode)
})
]
}
}