commit
df4c964d12
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env", {
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
],
|
||||
"@babel/preset-react"
|
||||
],
|
||||
"plugins": ["@babel/plugin-proposal-object-rest-spread"],
|
||||
}
|
|
@ -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
|
||||
|
|
3
.sailsrc
3
.sailsrc
|
@ -5,5 +5,8 @@
|
|||
"_generatedWith": {
|
||||
"sails": "1.0.2",
|
||||
"sails-generate": "1.15.28"
|
||||
},
|
||||
"hooks": {
|
||||
"grunt": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'))
|
|
@ -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
|
|
@ -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.
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -0,0 +1,2 @@
|
|||
@import 'lib/default';
|
||||
@import 'shared/auth';
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -64,7 +64,5 @@ module.exports.http = {
|
|||
// var middlewareFn = skipper({ strict: true });
|
||||
// return middlewareFn;
|
||||
// })(),
|
||||
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = {
|
|||
* *
|
||||
***************************************************************************/
|
||||
|
||||
|
||||
// ╔═╗╔═╗╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗
|
||||
// ╠═╣╠═╝║ ║╣ ║║║ ║║╠═╝║ ║║║║║ ║ ╚═╗
|
||||
// ╩ ╩╩ ╩ ╚═╝╝╚╝═╩╝╩ ╚═╝╩╝╚╝ ╩ ╚═╝
|
||||
|
|
34
package.json
34
package.json
|
@ -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": {
|
||||
|
|
|
@ -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 %>
|
||||
|
|
|
@ -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">
|
|
@ -0,0 +1 @@
|
|||
<%- partial('../../.tmp/public/login.html') %>
|
|
@ -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)
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue