Initial commit

This commit is contained in:
John Lyon-Smith
2018-02-22 17:57:27 -08:00
commit e80f5490d5
196 changed files with 38982 additions and 0 deletions

35
website/src/App.js Normal file
View File

@@ -0,0 +1,35 @@
import React from 'react'
import './App.scss'
import { NavBar } from './Navigation'
import { Home } from './Home'
import { Login, Logout, ResetPassword, ForgotPassword, ConfirmEmail, ProtectedRoute } from './Auth'
import { Dashboard } from './Dashboard'
import { Profile } from './Profile'
import { Users } from './Users'
import { Footer } from './Footer'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
export class App extends React.Component {
render() {
return (
<Router basename='/'>
<div className='App'>
<NavBar />
<Switch>
<Route exact path='/' component={Home} />
<Route path='/login' component={Login} />
<Route path='/confirm-email' component={ConfirmEmail} />
<Route path='/reset-password' component={ResetPassword} />
<Route path='/forgot-password' component={ForgotPassword} />
<ProtectedRoute path='/logout' component={Logout} />
<ProtectedRoute path='/profile' component={Profile} />
<ProtectedRoute roles={['administrator', 'normal']} path='/dashboard' component={Dashboard} />
<ProtectedRoute roles={['administrator']} path='/users' component={Users} />
<Route component={Home} />{/* No Match Route */}
</Switch>
<Footer />
</div>
</Router>
)
}
}

9
website/src/App.scss Normal file
View File

@@ -0,0 +1,9 @@
.App {
text-align: center;
}
@media screen and (max-width: 768px) {
html, body {
padding-top: 4em;
}
}

View File

@@ -0,0 +1,70 @@
import React from 'react'
import { api } from '../helpers'
import PropTypes from 'prop-types'
import { Container } from 'semantic-ui-react'
import { MessageDialog, WaitDialog } from '../Dialog'
import './ConfirmEmail.scss'
export class ConfirmEmail extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
}
constructor() {
super()
this.state = {
waitDialog: null,
messageDialog: null
}
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
}
componentDidMount(props) {
let emailToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('email-token')
this.setState({ waitDialog: { message: 'Validating Email...' } })
if (emailToken) {
api.confirmEmail(emailToken).then((response) => {
this.setState({ waitDialog: null })
if (response && response.passwordToken) {
// API will send a password reset token if this is the first time loggin on
this.props.history.replace(`/reset-password?password-token=${response.passwordToken}`)
} else {
this.props.history.replace('/login')
}
}).catch((err) => {
console.error(err)
const supportEmail = 'support@kingstonsoftware.solutions' // TODO: From configuration
const message = err.message.includes('The token was not found')
? 'This email address may have already been confirmed.'
: `Please contact ${supportEmail} to request a new user invitation`
this.setState({
waitDialog: null,
messageDialog: {
title: 'Error Verifying Email...',
message: `We couldn't complete that request. ${message}`
}
})
})
} else {
this.props.history.replace('/login')
}
}
handleMessageDialogDismiss() {
this.setState({ messageDialog: null })
this.props.history.replace('/login')
}
render() {
return (
<Container className='email-confirm-container'>
<WaitDialog active={!!this.state.waitDialog}
message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
<MessageDialog error open={!!this.state.messageDialog}
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
onDismiss={this.handleMessageDialogDismiss} />
</Container>
)
}
}

View File

@@ -0,0 +1,11 @@
.ui.container.email-confirm-container {
display: flex;
height: 80vh;
flex-direction: column;
align-items: center;
justify-content: center;
}
.ui.container.email-confirm-container button {
margin-top: 1em;
}

View File

@@ -0,0 +1,93 @@
import React from 'react'
import PropTypes from 'prop-types'
import { regExpPattern } from 'regexp-pattern'
import { Container, Header, Form, Message } from 'semantic-ui-react'
import './ForgotPassword.scss'
import { MessageDialog, WaitDialog } from '../Dialog'
import { Validator, ValidatedInput, ValidatedButton } from '../Validated'
import { api } from '../helpers'
export class ForgotPassword extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
}
static validations = {
email: {
alwaysGet: true,
isValid: (r, v) => (regExpPattern.email.test(v))
},
submit: {
nonValue: true,
isDisabled: (r) => (!r.anyModified || !r.allValid)
}
}
constructor(props) {
super(props)
this.state = {
validator: new Validator({}, ForgotPassword.validations),
messageDialog: null,
waitDialog: null
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
}
handleSubmit() {
const obj = this.state.validator.getValues()
this.setState({ waitDialog: { message: 'Requesting Reset Email' } })
api.sendResetPassword(obj.email).then((res) => {
const email = this.state.validator.getField('email').value
this.setState({
waitDialog: null,
messageDialog: {
error: false,
title: 'Password Reset Requested',
message: `An email will be sent to '${email}' with a reset link. Please click on it to finish resetting the password.`
}
})
}).catch((error) => {
this.setState({
validator: new Validator({}, ForgotPassword.validations), // Reset to avoid rapid retries
waitDialog: null,
messageDialog: {
error: true,
title: 'Password Reset Failed',
message: `There was a problem requesting the password reset. ${error ? error.message : ''}`
}
})
})
}
handleMessageDialogDismiss() {
this.props.history.replace('/')
}
render() {
return (
<Container className='forgot-password-container'>
<Header content='Forgotten Password' size='large' />
<Form size='large' onSubmit={this.handleSubmit}>
<ValidatedInput label='Email' name='email'
placeholder='example@xyz.com' validator={this.state.validator}
message='A valid email address' />
<Message info content='The email address of an existing user to send the password reset link to.' />
<ValidatedButton className='submit' name='submit' content='Submit'
primary submit validator={this.state.validator}>Submit</ValidatedButton>
</Form>
<WaitDialog active={!!this.state.waitDialog}
message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
<MessageDialog
open={!!this.state.messageDialog}
error={this.state.messageDialog ? this.state.messageDialog.error : true}
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
onDismiss={this.handleMessageDialogDismiss} />
</Container>
)
}
}

View File

@@ -0,0 +1,23 @@
.ui.container.forgot-password-container {
height: 80vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.ui.container.forgot-password-container .header {
border-bottom: 1px solid #d4d4d5;
width: 40%;
padding-bottom: 0.5em;
margin-bottom: 1em;
}
.ui.container.forgot-password-container form {
width: 40%;
}
.ui.container.forgot-password-container .message {
margin: 2em 0;
font-size: 0.8em;
}

125
website/src/Auth/Login.js Normal file
View File

@@ -0,0 +1,125 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Container, Header, Form, Message } from 'semantic-ui-react'
import './Login.scss'
import { regExpPattern } from 'regexp-pattern'
import { api } from '../helpers'
import { Validator, ValidatedInput, ValidatedCheckbox, ValidatedButton } from '../Validated'
import { WaitDialog, MessageDialog } from '../Dialog'
import { Link } from 'react-router-dom'
export class Login extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
}
static validations = {
email: {
alwaysGet: true,
isValid: (r, v) => (regExpPattern.email.test(v))
},
password: {
alwaysGet: true,
isValid: (r, v) => (v !== '')
},
rememberMe: {
alwaysGet: true,
initValue: true
},
submit: {
nonValue: true,
isDisabled: (r) => (!r.anyModified || !r.allValid)
}
}
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
this.state = {
waitDialog: false,
messageDialog: null,
validator: new Validator({}, Login.validations)
}
}
handleSubmit(e) {
e.preventDefault()
e.stopPropagation()
if (this.state.messageDialog) {
this.setState({ messageDialog: null })
return
} else if (this.state.waitDialog) {
return
}
let obj = this.state.validator.getValues()
if (obj) {
this.setState({ waitDialog: true })
api.login(obj.email, obj.password, obj.rememberMe).then((user) => {
this.setState({ waitDialog: false })
if (this.props.history) {
const landing = user.role === 'broker' ? '/broker-dashboard' : 'dashboard'
let url = new URLSearchParams(window.location.search).get('redirect') || landing
try {
this.props.history.replace(url)
} catch (error) {
this.props.history.replace('/')
}
}
}).catch((error) => {
const elems = document.getElementsByName('email')
if (elems) {
elems[0].focus()
}
this.setState({
validator: new Validator({ email: this.state.validator.getField('email').value }, Login.validations),
waitDialog: false,
messageDialog: { title: 'Login Error...', message: `Unable to login. ${error.message}` }
})
})
}
}
handleMessageDialogDismiss() {
this.setState({ messageDialog: null })
}
render() {
return (
<Container id='login' className='password-reset-container'>
<Header size='large'>Login Portal</Header>
<Form onSubmit={this.handleSubmit}>
{/* Add in 'Username' field pass */}
<ValidatedInput label='Email' name='email'
placeholder='example@xyz.com' validator={this.state.validator}
message='Enter the email address associated with your account.' width={16} />
<ValidatedInput password label='Password' name='password'
validator={this.state.validator} message='Enter your password.' width={16} />
<Form.Group widths='equal' className='login-options'>
<Form.Field className='login-password'>
<Link to='/forgot-password'>Forgot your password?</Link>
</Form.Field>
<ValidatedCheckbox label='Remember Me'
name='rememberMe' onChange={this.handleChange} validator={this.state.validator}
message='Should we keep you logged in on this computer?' className='login-checkbox' />
</Form.Group>
<ValidatedButton className='submit' name='submit' content='Submit'
primary submit validator={this.state.validator} />
<Message info>
Please contact <a href='mailto:support@jamoki.com'>support@jamoki.com</a> to request login credentials.
</Message>
</Form>
<WaitDialog active={this.state.waitDialog} message='Logging In' />
<MessageDialog error open={!!this.state.messageDialog}
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
onDismiss={this.handleMessageDialogDismiss} />
</Container>
)
}
}

View File

@@ -0,0 +1,3 @@
#login .login-options { margin: 1.5em 0 3em 0; }
#login .login-password { text-align: left; }
#login .login-checkbox { text-align: right; }

View File

@@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import { api } from '../helpers'
export class Logout extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
}
componentDidMount(event) {
api.logout().then(() => {
if (this.props.history) {
this.props.history.push('/')
}
})
}
render() {
return null
}
}

View File

@@ -0,0 +1,53 @@
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { PropTypes } from 'prop-types'
import { api } from '../helpers'
export class ProtectedRoute extends React.Component {
static propTypes = {
roles: PropTypes.array,
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string
})
}
constructor(props) {
super(props)
this.updateComponent = this.updateComponent.bind(this)
}
updateComponent() {
this.forceUpdate()
}
componentDidMount() {
api.addListener('login', this.updateComponent)
}
componentWillUnmount() {
api.removeListener('login', this.updateComponent)
}
render(props) {
let user = api.loggedInUser
if (user) {
if (user.pending) {
// The Api might be in the middle of fetching the user information
return <div />
}
let roles = this.props.roles
if (!roles || roles.includes(user.role)) {
return <Route {...this.props} />
} else if (!!user.role && user.role === 'broker') {
return <Redirect to='/broker-dashboard' />
} else if (!!user.role && (user.role === 'employee' || 'administrator' || 'executive')) {
return <Redirect to='/dashboard' />
}
} else {
return <Redirect to={`/login?redirect=${this.props.location.pathname}${this.props.location.search}`} />
}
}
}

View File

@@ -0,0 +1,93 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Container, Header, Form, Message } from 'semantic-ui-react'
import './ResetPassword.scss'
import { Validator, ValidatedInput, ValidatedButton } from '../Validated'
import { MessageDialog, WaitDialog } from '../Dialog'
import { api } from '../helpers'
export class ResetPassword extends React.Component {
static propTypes = {
history: PropTypes.oneOfType([PropTypes.array, PropTypes.object])
}
static validations = {
newPassword: {
alwaysGet: true,
isValid: (r, v) => (v.length >= 6)
},
reenteredNewPassword: {
isValid: (r, v) => (v !== '' && v === r.getField('newPassword').value)
},
submit: {
nonValue: true,
isDisabled: (r) => (!r.anyModified && !r.allValid)
}
}
constructor(props) {
super(props)
this.state = {
validator: new Validator({}, ResetPassword.validations),
messageDialog: null,
waitDialog: null
}
this.handleSubmit = this.handleSubmit.bind(this)
this.handleMessageDialogDismiss = this.handleMessageDialogDismiss.bind(this)
}
handleSubmit() {
const obj = this.state.validator.getValues()
const passwordToken = new URLSearchParams(decodeURIComponent(window.location.search)).get('password-token')
this.setState({ waitDialog: { message: 'Setting Password...' } })
api.resetPassword({ newPassword: obj.newPassword, passwordToken }).then(() => {
this.setState({ waitDialog: null })
this.props.history.replace('/login')
}).catch((error) => {
this.setState({
validator: new Validator({}, ResetPassword.validations), // Reset to avoid accidental rapid retries
waitDialog: null,
messageDialog: {
title: 'We had a problem changing your password',
message: error.message
}
})
})
}
handleMessageDialogDismiss() {
this.setState({ messageDialog: null })
}
render() {
return (
<Container className='password-reset-container'>
<Header content='Reset Password' size='large' />
<Form size='large' onSubmit={this.handleSubmit}>
<ValidatedInput label='New Password' password name='newPassword'
message='A new password, cannot be blank or the same as your old password'
width={16} validator={this.state.validator} />
<ValidatedInput label='Re-entered New Password' password name='reenteredNewPassword'
message='The new password again, must match and cannot be blank'
width={16} validator={this.state.validator} />
<Message info>
Passwords can contain special characters and are discouraged from being simple or reused from other sites or applications.
<br /><br />
Passwords must be at least 6 characters long.
</Message>
<ValidatedButton className='submit' name='submit' content='Submit'
primary submit validator={this.state.validator} />
</Form>
<MessageDialog error open={!!this.state.messageDialog}
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
onDismiss={this.handleMessageDialogDismiss} />
<WaitDialog active={!!this.state.waitDialog}
message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
</Container>
)
}
}

View File

@@ -0,0 +1,23 @@
.ui.container.password-reset-container {
height: 80vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.ui.container.password-reset-container .header {
border-bottom: 1px solid #d4d4d5;
width: 40%;
padding-bottom: 0.5em;
margin-bottom: 1em;
}
.ui.container.password-reset-container form {
width: 40%;
}
.ui.container.password-reset-container .message {
margin: 2em 0;
font-size: 0.8em;
}

View File

@@ -0,0 +1,6 @@
export { Login } from './Login'
export { Logout } from './Logout'
export { ResetPassword } from './ResetPassword'
export { ForgotPassword } from './ForgotPassword'
export { ConfirmEmail } from './ConfirmEmail'
export { ProtectedRoute } from './ProtectedRoute'

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { Container } from 'semantic-ui-react'
import { ProjectList } from './ProjectList'
import { api } from '../helpers'
export class Dashboard extends React.Component {
constructor(props) {
super(props)
this.state = {
projects: []
}
}
componentDidMount() {
api.listDashboardProjects().then((list) => {
this.setState({
projects: list.items
})
}).catch((error) => {
console.error(error)
})
}
render() {
return (
<Container>
<ProjectList projects={this.state.projects} />
</Container>
)
}
}

View File

@@ -0,0 +1,60 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Card, Image, Button, Icon, Popup } from 'semantic-ui-react'
import { Link } from 'react-router-dom'
import { Constants, api } from '../helpers'
import './ProjectCard.scss'
export class ProjectCard extends React.Component {
static propTypes = {
project: PropTypes.object.isRequired // TODO: Add required fields
}
render() {
let { project } = this.props
const corporationLink = `/corporations/${project.branch.corporation.fingerprint}`
const branchLink = corporationLink + `/branches/${project.branch.fingerprint}`
const projectLink = branchLink + `/projects/${project.fingerprint}`
return (
<Card className='project-card' fluid>
{/* Project/Corp Logo Link Section */}
<Card.Content>
<Image as={Link} to={corporationLink}
src={api.makeImageUrl(project.branch.corporation.imageId, Constants.logoImageSize)}
alt={project.branch.corporation.name} />
</Card.Content>
{/* Project Info Section */}
<Card.Content className='project-info'>
<Card.Header as={Link}
to={projectLink}
className='project-name' content={project.name} />
<Card.Description>
<Link to={branchLink}>{project.branch.name}</Link>
&nbsp;@&nbsp;
<Link to={corporationLink}>{project.branch.corporation.name}</Link>
</Card.Description>
</Card.Content>
{/* Project Link and Status Icon */}
<Card.Content className='project-links'>
<Button fluid as={Link} to={projectLink}
className='project-link' content='View Project' />
{ project.error
? <Popup trigger={<Icon name='check' size='large' />}
content='This project is underway with no errors!'
position='bottom left' />
: <Popup trigger={<Icon name='table' size='large' color='red' />}
content='The Client Data Sheet for this project has not been completed.'
position='bottom left' />
}
</Card.Content>
{/* Project Status Bar */}
<div className={`status in-progress`}>
In-Progress
</div>
</Card>
)
}
}

View File

@@ -0,0 +1,55 @@
.ui.card.project-card {
border-top: 2px solid #D4D4D5;
}
.status {
color: WhiteSmoke;
padding: 0.25em 1em;
text-align: right;
font-size: 0.85em;
text-transform: capitalize;
}
.in-progress { background-color: #CF414A; }
.sending { background-color: #D66C0A; }
.receiving { background-color: #0062AF; }
.project-card img {
height: 2.5em !important;
width: auto !important;
margin: 0.25em auto;
}
.project-info {
height: 7em !important;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.project-info a {
padding: 0.25em;
color: black;
border-radius: 5px;
}
.project-name.header { font-size: 1em !important; }
.project-links {
padding: 0 !important;
display: flex;
flex-direction: row;
align-items: center;
}
.project-links a {
background-color: transparent !important;
}
.project-links i.icon {
position: absolute;
right: 0.25em;
color: LightSlateGrey;
}

View File

@@ -0,0 +1,32 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Grid } from 'semantic-ui-react'
import { ProjectCard } from './ProjectCard'
import './ProjectList.scss'
export class ProjectList extends React.Component {
static propTypes = {
projects: PropTypes.arrayOf(PropTypes.object)
}
render() {
let { projects } = this.props
return (
<div className='project-list'>
<Grid centered={(window.innerWidth <= 768)}>
<Grid.Column width={16} textAlign='left' className='project-list-heading'>
{projects.length} Projects Underway
</Grid.Column>
{this.props.projects
? projects.map((project, index) => (
<Grid.Column mobile={12} tablet={8} computer={4} textAlign='center' key={index}>
<ProjectCard project={project} />
</Grid.Column>))
: null
}
</Grid>
</div>
)
}
}

View File

@@ -0,0 +1,11 @@
.project-list {
margin: 3em 0 2em 0;
padding-bottom: 2em;
border-bottom: 1px solid Silver;
}
.project-list-heading {
font-size: 1.5em;
border-bottom: 1px solid Silver;
margin-bottom: 1em !important;
}

View File

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

View File

@@ -0,0 +1,85 @@
import React from 'react'
import PropTypes from 'prop-types'
import { autoBind } from 'auto-bind2'
import { Modal, Button, Icon, Header, Grid, Form } from 'semantic-ui-react'
import { ValidatedInput, ValidatedActionsButton, Validator } from '../Validated'
import { regExpPattern } from 'regexp-pattern'
export class ChangeEmailDialog extends React.Component {
static propTypes = {
open: PropTypes.bool,
onDismiss: PropTypes.func
}
static validations = {
newEmail: {
isValid: (r, v) => (v !== '' && regExpPattern.email.test(v))
},
submit: {
isDisabled: (r) => (!r.allValid),
nonValue: true
}
}
constructor(props) {
super(props)
autoBind(this, (name) => (name.startsWith('handle')))
this.state = {
validator: new Validator({}, ChangeEmailDialog.validations)
}
}
close(newEmail) {
this.state.validator = new Validator({}, ChangeEmailDialog.validations)
this.props.onDismiss(newEmail)
}
handleClose() {
this.close(null)
}
handleSubmit(e) {
e.preventDefault()
let newEmail = null
if (this.state.validator.anyModified && this.state.validator.allValid) {
newEmail = this.state.validator.getField('newEmail').value
}
this.close(newEmail)
}
handleClick(e) {
this.close(null)
}
render() {
return (
<Modal dimmer='inverted' open={this.props.open} onClose={this.handleClose}
closeOnDimmerClick={false}>
<Header color='black' icon='edit' content='Change Email' />
<Modal.Content>
<Form className='user-form' id='emailForm' onSubmit={this.handleSubmit}>
<Grid>
<Grid.Column width={16}>
<Form.Group>
<ValidatedInput label='New Email' name='newEmail' width={16}
message='Your new email address, e.g. xyz@abc.com, cannot be blank'
validator={this.state.validator} />
</Form.Group>
</Grid.Column>
</Grid>
</Form>
</Modal.Content>
<Modal.Actions>
<ValidatedActionsButton primary submit form='emailForm' name='submit' validator={this.state.validator}>
<Icon name='checkmark' /> OK
</ValidatedActionsButton>
<Button color='red' onClick={this.handleClick}>
<Icon name='close' /> Cancel
</Button>
</Modal.Actions>
</Modal>
)
}
}

View File

@@ -0,0 +1,102 @@
import React from 'react'
import PropTypes from 'prop-types'
import { autoBind } from 'auto-bind2'
import { Modal, Button, Icon, Header, Grid, Form } from 'semantic-ui-react'
import { ValidatedInput, ValidatedActionsButton, Validator } from '../Validated'
export class ChangePasswordDialog extends React.Component {
static propTypes = {
open: PropTypes.bool,
onDismiss: PropTypes.func
}
static validations = {
oldPassword: {
alwaysGet: true,
isValid: (r, v) => (v !== '')
},
newPassword: {
alwaysGet: true,
isValid: (r, v) => (v !== '' && v !== r.fields.oldPassword.value)
},
reenteredNewPassword: {
alwaysGet: true,
isValid: (r, v) => (v !== '' && v === r.fields.newPassword.value)
},
submit: {
isDisabled: (r) => (!r.allValid),
nonValue: true
}
}
constructor(props) {
super(props)
autoBind(this, (name) => (name.startsWith('handle')))
this.state = {
validator: new Validator({}, ChangePasswordDialog.validations)
}
}
close(passwords) {
this.state.validator = new Validator({}, ChangePasswordDialog.validations)
this.props.onDismiss(passwords)
}
handleClose() {
close(null)
}
handleSubmit(e) {
e.preventDefault()
let passwords = null
if (this.state.validator.allValid) {
const oldPassword = this.state.validator.getField('oldPassword').value
const newPassword = this.state.validator.getField('newPassword').value
passwords = { oldPassword, newPassword }
}
this.close(passwords)
}
handleClick(e) {
this.close(null)
}
render() {
return (
<Modal dimmer='inverted' open={this.props.open} onClose={this.handleClose}
closeOnDimmerClick={false}>
<Header color='black' icon='edit' content='Change Password' />
<Modal.Content>
<Form className='user-form' id='passwordForm' onSubmit={this.handleSubmit}>
<Grid stackable>
<Grid.Column width={16}>
<Form.Group>
<ValidatedInput label='Current Password' password name='oldPassword'
message='Your existing password, cannot be blank'
width={8} validator={this.state.validator} />
</Form.Group>
<Form.Group>
<ValidatedInput label='New Password' password name='newPassword'
message='A new password, cannot be blank or the same as your old password'
width={8} validator={this.state.validator} />
<ValidatedInput label='Re-entered New Password' password name='reenteredNewPassword'
message='The new password again, must match and cannot be blank'
width={8} validator={this.state.validator} />
</Form.Group>
</Grid.Column>
</Grid>
</Form>
</Modal.Content>
<Modal.Actions>
<ValidatedActionsButton primary submit form='passwordForm' name='submit' validator={this.state.validator}>
<Icon name='checkmark' /> OK
</ValidatedActionsButton>
<Button color='red' onClick={this.handleClick}>
<Icon name='close' /> Cancel
</Button>
</Modal.Actions>
</Modal>
)
}
}

View File

@@ -0,0 +1,33 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Modal, Button, Icon, Header } from 'semantic-ui-react'
export class MessageDialog extends React.Component {
static propTypes = {
open: PropTypes.bool,
error: PropTypes.bool,
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
onDismiss: PropTypes.func
}
render() {
return (
<Modal dimmer='inverted' open={this.props.open} onClose={this.props.onDismiss}
closeOnDimmerClick={false}>
<Header
color={this.props.error ? 'red' : 'blue'}
icon={this.props.error ? 'warning circle' : 'info circle'}
content={this.props.title} />
<Modal.Content>
<h3>{this.props.message}</h3>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.props.onDismiss}>
<Icon name='checkmark' /> OK
</Button>
</Modal.Actions>
</Modal>
)
}
}

View File

@@ -0,0 +1,26 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Dimmer, Button, Progress } from 'semantic-ui-react'
import './ProgressDialog.scss'
export class ProgressDialog extends React.Component {
static propTypes = {
open: PropTypes.bool.isRequired,
message: PropTypes.string.isRequired,
percent: PropTypes.number.isRequired,
onCancel: PropTypes.func
}
render() {
return (
<Dimmer inverted page active={this.props.open}>
<div className='progress-dialog'>
<Progress id='progress' percent={this.props.percent}
size='large' progress indicating autoSuccess>{this.props.message}
</Progress>
<Button name='cancel' primary onClick={this.props.onCancel}>Cancel</Button>
</div>
</Dimmer>
)
}
}

View File

@@ -0,0 +1,11 @@
.progress-dialog {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
#progress {
width: 60%;
}

View File

@@ -0,0 +1,18 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Dimmer, Loader } from 'semantic-ui-react'
export class WaitDialog extends React.Component {
static propTypes = {
active: PropTypes.bool.isRequired,
message: PropTypes.string.isRequired
}
render() {
return (
<Dimmer inverted page active={this.props.active}>
<Loader inverted size='massive'>{this.props.message}...</Loader>
</Dimmer>
)
}
}

View File

@@ -0,0 +1,37 @@
import React from 'react'
import PropTypes from 'prop-types'
import './YesNoMessageDialog.scss'
import { Modal, Button, Icon, Header } from 'semantic-ui-react'
export class YesNoMessageDialog extends React.Component {
static propTypes = {
open: PropTypes.bool,
title: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
onDismiss: PropTypes.func
}
onDismiss(yes) {
return () => (this.props.onDismiss ? this.props.onDismiss(yes) : null)
}
render() {
return (
<Modal dimmer='inverted' open={this.props.open} onClose={this.onDismiss(false)}
closeOnDimmerClick={false} className='yes-no-modal'>
<Header color='orange' size='large' icon='warning circle' content={this.props.title} />
<Modal.Content>
<p>{this.props.message}</p>
</Modal.Content>
<Modal.Actions>
<Button negative onClick={this.onDismiss(false)}>
<Icon name='remove' /> No
</Button>
<Button positive onClick={this.onDismiss(true)}>
<Icon name='checkmark' /> Yes
</Button>
</Modal.Actions>
</Modal>
)
}
}

View File

@@ -0,0 +1,6 @@
.yes-no-modal p {
font-size: 1.125em;
line-height: 1.6em;
width: 90%;
margin: auto;
}

View File

@@ -0,0 +1,6 @@
export { WaitDialog } from './WaitDialog'
export { ProgressDialog } from './ProgressDialog'
export { YesNoMessageDialog } from './YesNoMessageDialog'
export { MessageDialog } from './MessageDialog'
export { ChangePasswordDialog } from './ChangePasswordDialog'
export { ChangeEmailDialog } from './ChangeEmailDialog'

View File

@@ -0,0 +1,106 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Button, Header, Icon } from 'semantic-ui-react'
import { autoBind } from 'auto-bind2'
import './FilePicker.scss'
export class FilePicker extends React.Component {
static propTypes = {
validExtensions: PropTypes.arrayOf(PropTypes.string).isRequired, // Make sure these are lowercase
onFileSelect: PropTypes.func,
content: PropTypes.string,
horizontal: PropTypes.bool
}
constructor(props) {
super(props)
autoBind(this, (name) => (name.startsWith('handle')))
this.state = {
icon: 'none',
value: ''
}
this.counter = 0
}
handleDragEnter(e) {
this.counter++
this.setState({ icon: 'pending' })
}
handleDragLeave(e) {
this.counter--
if (this.counter === 0) {
this.setState({ icon: 'none' })
}
}
handleDragOver(e) {
e.preventDefault()
e.stopPropagation()
}
handleDragDrop(e) {
e.preventDefault()
const files = e.dataTransfer.files
if (files.length > 0) {
const file = files[0]
if (file) {
const fileName = file.name.toLowerCase()
const isValidFile = this.props.validExtensions.some((ext) => (fileName.endsWith(ext)))
this.setState({ icon: isValidFile ? 'valid' : 'invalid' })
this.counter = 0
if (isValidFile && this.props.onFileSelect) {
this.props.onFileSelect(file)
window.setTimeout(function() {
this.setState({ icon: 'none' })
}.bind(this), 1000)
}
}
}
}
handleOnFileChange(e) {
let file = e.currentTarget.files[0]
this.setState({ value: '' })
if (this.props.onFileSelect) {
this.props.onFileSelect(file)
}
}
render() {
return (
<div draggable id={this.props.horizontal ? 'holderHorizontal' : 'holderVertical'}
onDragOver={this.handleDragOver}
onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave}
onDrop={this.handleDragDrop}>
<input id='fileInput' type='file' onChange={this.handleOnFileChange} value={this.state.value} />
<Button as='label' htmlFor='fileInput' size={'medium'} type='browse' name='selectFile'
id='selectFile' content={this.props.content} icon='upload' primary />
{ this.props.horizontal
? (<Icon id='fileIconHorizontal'
name={'file' + ((this.state.icon === 'none' || this.state.icon === 'pending') ? ' outline' : '')}
size='huge'
color={this.state.icon === 'valid' ? 'green' : this.state.icon === 'invalid' ? 'red' : 'grey'}
style={this.state.icon === 'pending' ? { transform: 'scale(1.5)' } : {}} />)
: (<Header as='h3' color='grey' icon>
OR
<Header.Subheader>
Drag-and-drop the file here
</Header.Subheader>
<Icon id='fileIconVertical' className=''
name={'file' + ((this.state.icon === 'none' || this.state.icon === 'pending') ? ' outline' : '')}
size='huge'
color={this.state.icon === 'valid' ? 'green' : this.state.icon === 'invalid' ? 'red' : 'grey'}
style={this.state.icon === 'pending' ? { transform: 'scale(1.5)' } : {}} />
</Header>)
}
</div>
)
}
}

View File

@@ -0,0 +1,51 @@
#holderVertical {
border: 3px dashed lightgray;
text-align: center;
border-radius: 0.5em;
display: flex;
flex-direction: column;
align-items: center;
padding: 0.25em 1.25em;
width: auto;
overflow: hidden;
}
#holderHorizontal {
border: 3px dashed lightgray;
text-align: center;
border-radius: 0.5em;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 0.25em 1.25em;
width: auto;
}
#helpText {
height: 2em;
margin-top: 2em;
margin-bottom: 2em;
color: lightgray;
}
#fileIconVertical {
margin-top: 0.5em;
margin-bottom: 0.5em;
transition: all 0.3s ease;
}
#fileIconHorizontal {
margin-left: 0.3em;
transition: all 0.3s ease;
}
#selectFile {
margin-top: 1.5em;
margin-bottom: 1.5em;
white-space: nowrap;
}
#fileInput {
display: none;
}

View File

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

View File

@@ -0,0 +1,9 @@
import React from 'react'
import './Footer.scss'
import { VersionInfo } from '../version'
export const Footer = () => (
<footer>
v{VersionInfo.fullVersion}. &copy; {VersionInfo.startYear} Deighton. All rights reserved.
</footer>
)

View File

@@ -0,0 +1,14 @@
footer {
background-color: #0b0b0b;
color: grey;
height: 3em;
line-height: 3em;
padding: 0 2em;
font-size: 1em;
margin-top: 3em;
position: absolute;
right: 0;
bottom: 0;
left: 0;
text-align: left;
}

View File

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

9
website/src/Home/Home.js Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
export class Home extends React.Component {
render() {
return (
<div />
)
}
}

View File

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

View File

@@ -0,0 +1,169 @@
import React from 'react'
import PropTypes from 'prop-types'
import { autoBind } from 'auto-bind2'
import { Menu, Image, Button, Dropdown } from 'semantic-ui-react'
import { Link, NavLink } from 'react-router-dom'
import logoImg from 'images/logo.png'
import { Constants, api } from '../helpers'
import './NavBar.scss'
export class NavBar extends React.Component {
static propTypes = {
activeItem: PropTypes.string
}
constructor() {
super()
autoBind(this, (name) => (name.startsWith('handle')))
this.state = {
activeItem: null
}
}
handleItemClick = (event, { name }) => this.setState({ activeItem: name })
handleUpdate() {
this.forceUpdate()
}
componentDidMount() {
api.addListener('login', this.handleUpdate)
api.addListener('logout', this.handleUpdate)
api.addListener('newThumbnailImage', this.handleUpdate)
}
componentWillUnmount() {
api.removeListener('login', this.handleUpdate)
api.removeListener('logout', this.handleUpdate)
api.addListener('newThumbnailImage', this.handleUpdate)
}
render() {
const { activeItem } = this.state
const user = api.loggedInUser
if (!user) {
if (window.innerWidth <= 768) {
return (
<Menu fluid inverted borderless fixed='top' size='large' id='MobileNavMenu'>
<Menu.Item>
<Image as={Link} to='/' src={logoImg} alt='Deighton Logo' size='tiny' centered />
</Menu.Item>
<Menu.Menu position='right'>
<Menu.Item>
<Button as={Link} to='/login' color='facebook' content='Login' />
</Menu.Item>
</Menu.Menu>
</Menu>
)
} else {
return (<Menu pointing stackable secondary id='NavMenu'>
<Menu.Item link href='/' active={activeItem === 'logo'} onClick={this.handleItemClick}>
<Image className='menu-logo' src={logoImg} alt='Deighton Logo' />
</Menu.Item>
<Menu.Item id='nav-item1' name='meetTheTeam' active={activeItem === 'meetTheTeam'} onClick={this.handleItemClick} />
<Menu.Item id='nav-item2' name='testimonials' active={activeItem === 'testimonials'} onClick={this.handleItemClick} />
<Menu.Item id='nav-item3' name='contactUs' active={activeItem === 'contactUs'} onClick={this.handleItemClick} />
<Menu.Menu position='right'>
<Button as={Link} to='/login' primary content='Login' id='login-button' />
</Menu.Menu>
</Menu>)
}
}
if (user.pending) {
return <div />
}
const userImageUrl = api.makeImageUrl(user.thumbnailImageId, Constants.smallUserImageSize)
const userName = user.firstName + ' ' + user.lastName
if (window.innerWidth <= 768) {
return (
user.role === 'broker'
? <Menu fluid inverted borderless fixed='top' size='large' id='MobileNavMenu'>
<Menu.Item>
<Image as={Link} to='/' src={logoImg} alt='Deighton Logo' size='tiny' centered />
</Menu.Item>
<Menu.Menu position='right'>
<Dropdown icon='content' item>
<Dropdown.Menu>
<Dropdown.Item as={Link} exact to='/'>Home</Dropdown.Item>
<Dropdown.Item as={Link} to='/broker-dashboard'>Dashboard</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item as={Link} to='/profile'>Profile</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item as={Link} to='/logout'>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Menu.Menu>
</Menu>
: <Menu fluid inverted borderless fixed='top' size='large' id='MobileNavMenu'>
<Menu.Item>
<Image as={Link} to='/' src={logoImg} alt='Deighton Logo' size='tiny' centered />
</Menu.Item>
<Menu.Menu position='right'>
<Dropdown icon='content' item>
<Dropdown.Menu>
<Dropdown.Item as={Link} exact to='/'>Home</Dropdown.Item>
<Dropdown.Item as={Link} to='/dashboard'>Dashboard</Dropdown.Item>
<Dropdown.Item as={Link} to='/corporations'>Corporations</Dropdown.Item>
{ api.loggedInUser.role === 'administrator' || api.loggedInUser.role === 'executive'
? <Dropdown.Item as={Link} to='/forms'>Forms</Dropdown.Item>
: null
}
{ api.loggedInUser.role === 'administrator' || api.loggedInUser.role === 'executive'
? <Dropdown.Item as={Link} to='/users'>Users</Dropdown.Item>
: null
}
<Dropdown.Divider />
<Dropdown.Item as={Link} to='/profile'>Profile</Dropdown.Item>
<Dropdown.Divider />
<Dropdown.Item as={Link} to='/logout'>Logout</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
</Menu.Menu>
</Menu>
)
}
return (
user.role === 'broker'
? <Menu pointing secondary stackable id='NavMenu'>
<Menu.Item as={NavLink} activeClassName='nav-menu-active' exact to='/' name='home' active={activeItem === 'home'} onClick={this.handleItemClick} />
<Menu.Item as={NavLink} activeClassName='nav-menu-active' to='/broker-dashboard' name='dashboard' active={activeItem === 'dashboard'} onClick={this.handleItemClick} />
<Menu.Menu position='right'>
<div className='logged-in-user'>
<NavLink to='/profile' activeClassName='nav-menu-active'>
<Image avatar src={this.userImage} />
<span className='user-name'>{userName}</span>
</NavLink>
<Button as={Link} to='/logout' content='Logout' id='logout-button' />
</div>
</Menu.Menu>
</Menu>
: <Menu pointing secondary stackable id='NavMenu'>
<Menu.Item as={NavLink} activeClassName='nav-menu-active' exact to='/' name='home' active={activeItem === 'home'} onClick={this.handleItemClick} />
<Menu.Item as={NavLink} activeClassName='nav-menu-active' to='/dashboard' name='dashboard' active={activeItem === 'dashboard'} onClick={this.handleItemClick} />
<Menu.Item as={NavLink} activeClassName='nav-menu-active' to='/corporations' name='corporations' active={activeItem === 'corporations'} onClick={this.handleItemClick} />
{ api.loggedInUser.role === 'administrator' || api.loggedInUser.role === 'executive'
? <Menu.Item as={NavLink} activeClassName='nav-menu-active' to='/forms' name='forms' active={activeItem === 'forms'} onClick={this.handleItemClick} />
: null
}
{ api.loggedInUser.role === 'administrator' || api.loggedInUser.role === 'executive'
? <Menu.Item as={NavLink} activeClassName='nav-menu-active' to='/users' name='users' active={activeItem === 'users'} onClick={this.handleItemClick} />
: null
}
<Menu.Menu position='right'>
<div className='logged-in-user'>
<NavLink to='/profile' activeClassName='nav-menu-active'>
<Image avatar src={userImageUrl} />
<span className='user-name'>{userName}</span>
</NavLink>
<Button as={Link} to='/logout' content='Logout' id='logout-button' />
</div>
</Menu.Menu>
</Menu>
)
}
}

View File

@@ -0,0 +1,116 @@
/* === Nav Menu Stylesheet === */
#NavMenu {
background-color: #1b1b1b;
box-shadow: 0 0 20px 2px Grey;
}
#NavMenu a.item {
color: white;
align-self: flex-end;
}
#NavMenu a.item.nav-menu-active {
background-color: #2d2b2b;
height: 100%;
}
#NavMenu a.item.nav-menu-active:nth-child(n) { border-bottom: 0.25em solid #CF414A; }
#NavMenu a.item.nav-menu-active:nth-child(2n) { border-bottom: 0.25em solid #2185D0; }
#NavMenu a.item.nav-menu-active:nth-child(3n) { border-bottom: 0.25em solid #DB822E; }
#NavMenu a.item:hover { background-color: #444344; }
#NavMenu img.menu-logo {
height: 1em;
}
#login-button {
height: 60%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1em;
margin: auto 1em;
background-color: #3f62ab;
}
#logout-button {
height: 60%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1em;
margin: auto 1em;
background-color: silver;
}
.logged-in-user {
display: flex;
flex-direction: row;
align-items: center;
}
.logged-in-user a {
color: Silver;
margin-right: 1em;
}
.logged-in-user a:hover {
color: white;
}
.logged-in-user .user-name {
margin-left: 0.25em;
}
#nav-item1 { border-bottom: 5px solid #2e4d9a; }
#nav-item2 { border-bottom: 5px solid #db822e; }
#nav-item3 { border-bottom: 5px solid #cf414a; }
/* === Tablet-Specific Menu Styles === */
#MobileNavMenu {
border-radius: 0 !important;
border-bottom: 5px solid #db822e !important;
}
@media screen and (max-width: 992px) {
.user-name {
display: none;
}
}
/* === Mobile-Specific Menu Styles === */
@media screen and (max-width: 767px) {
#NavMenu {
font-size: 1em;
}
#NavMenu a.item {
text-align: center !important;
}
.logged-in-user {
width: 100%;
flex-direction: column;
align-items: flex-end;
}
.user-name {
display: inline;
}
.logged-in-user a {
width: 30%;
}
#logout-button {
width: 30%;
margin: 1.5em 0.5em 0.5em 0;
}
}

View File

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

View File

@@ -0,0 +1,177 @@
import React from 'react'
import { Container } from 'semantic-ui-react'
import { ProfileForm } from './ProfileForm'
import { Constants, api } from '../helpers'
import { WaitDialog, MessageDialog, ChangePasswordDialog, ProgressDialog, ChangeEmailDialog } from '../Dialog'
import { autoBind } from 'auto-bind2'
export class Profile extends React.Component {
constructor(props) {
super(props)
autoBind(this, (name) => (name.startsWith('handle')))
const user = api.loggedInUser
this.state = {
messageDialog: null,
waitDialog: null,
changePasswordDialog: null,
changeEmailDialog: null,
progressDialog: null,
uploadPercent: 0,
user,
userImageUrl: api.makeImageUrl(user.imageId, Constants.bigUserImageSize)
}
}
componentDidMount() {
api.addListener('newProfileImage', this.handleNewProfileImage)
}
componentWillUnmount() {
api.removeListener('newProfileImage', this.handleNewProfileImage)
}
handleNewProfileImage(data) {
this.setState({ userImageUrl: api.makeImageUrl(data.imageId, Constants.bigUserImageSize) })
}
handleSaved(user) {
this.setState({ waitDialog: { message: 'Updating Profile' } })
api.updateUser(user).then((updatedUser) => {
this.setState({
waitDialog: null,
user: updatedUser
})
}).catch((error) => {
this.setState({
waitDialog: null,
messageDialog: { title: 'Update Error...', message: `Unable to save the profile changes. ${error.message}` }
})
})
}
handleMessageDialogDismiss() {
this.setState({ messageDialog: null })
}
handleChangePassword() {
this.setState({ changePasswordDialog: true })
}
handleSelectImage(file) {
this.setState({ progressDialog: { message: `Uploading image '${file.name}'...`, file }, uploadPercent: 0 })
api.upload(file, this.handleProgress).then((uploadData) => {
this.setState({ progressDialog: null })
return api.setUserImage({
_id: api.loggedInUser._id,
imageId: uploadData.assetId,
bigSize: Profile.bigUserImageSize,
smallSize: Constants.smallUserImageSize
})
}).catch((error) => {
// TODO: if the upload succeeds but the setUserImage fails, delete the uploaded image
this.setState({
progressDialog: null,
messageDialog: { title: 'Upload Error...', message: `Unable to upload the file. ${error.message}` }
})
})
}
handleProgress(uploadData) {
if (this.state.progressDialog) {
this.setState({ uploadPercent: Math.round(uploadData.uploadedChunks / uploadData.numberOfChunks * 100) })
return true
} else {
return false
}
}
handleUploadCancel(result) {
this.setState({ progressDialog: null })
}
handleChangePasswordDismiss(passwords) {
this.setState({ changePasswordDialog: false })
if (passwords) {
this.setState({
waitDialog: { message: 'Changing Password' }
})
api.changePassword(passwords).then(() => {
this.setState({ waitDialog: false })
}).catch((error) => {
this.setState({
waitDialog: false,
messageDialog: {
title: 'Changing Password Error',
message: `Unable to change password. ${error.message}.`
}
})
})
}
}
handleChangeEmail() {
this.setState({ changeEmailDialog: {} })
}
handleChangeEmailDismiss(newEmail) {
this.setState({ changeEmailDialog: null })
if (!newEmail) {
return
}
this.setState({
waitDialog: { message: 'Requesting Email Change...' }
})
api.sendConfirmEmail({ newEmail }).then(() => {
this.setState({
waitDialog: null,
messageDialog: {
error: false,
title: 'Email Change Requested...',
message: `An email has been sent to '${newEmail}' with a link that you need to click on to finish changing your email.`
}
})
}).catch((error) => {
this.setState({
error: true,
waitDialog: null,
messageDialog: {
error: true,
title: 'Email Change Error...',
message: `Unable to request email change. ${error ? error.message : ''}`
}
})
})
}
render() {
return (
<Container>
<ProfileForm
user={this.state.user}
onSaved={this.handleSaved}
onSelectImage={this.handleSelectImage}
onChangePassword={this.handleChangePassword}
onChangeEmail={this.handleChangeEmail}
userImageUrl={this.state.userImageUrl} />
<MessageDialog error open={!!this.state.messageDialog}
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
onDismiss={this.handleMessageDialogDismiss} />
<ChangeEmailDialog open={!!this.state.changeEmailDialog} onDismiss={this.handleChangeEmailDismiss} />
<WaitDialog active={!!this.state.waitDialog} message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
<ChangePasswordDialog open={!!this.state.changePasswordDialog} onDismiss={this.handleChangePasswordDismiss} />
<ProgressDialog open={!!this.state.progressDialog}
message={this.state.progressDialog ? this.state.progressDialog.message : ''}
percent={this.state.uploadPercent}
onCancel={this.handleUploadCancel} />
</Container>
)
}
}

View File

@@ -0,0 +1,170 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Grid, Form, Image } from 'semantic-ui-react'
import { regExpPattern } from 'regexp-pattern'
import './ProfileForm.scss'
import { Constants } from '../helpers'
import { FilePicker } from '../FilePicker'
import { Validator, ValidatedInput, ValidatedDropdown, ValidatedButton, ValidatedDatePicker } from '../Validated'
export class ProfileForm extends React.Component {
static propTypes = {
user: PropTypes.object.isRequired,
onSaved: PropTypes.func.isRequired,
onModifiedChanged: PropTypes.func,
onChangePassword: PropTypes.func,
onChangeEmail: PropTypes.func,
onSelectImage: PropTypes.func,
userImageUrl: PropTypes.string
}
static validations = {
email: {
isValid: (r, v) => (v !== ''),
isDisabled: (r) => (!!r._id)
},
firstName: {
isValid: (r, v) => (v !== '')
},
lastName: {
isValid: (r, v) => (v !== '')
},
zip: {
isValid: (r, v) => (v === '' || regExpPattern.zip.test(v))
},
state: {
isValid: (r, v) => (v === '' || regExpPattern.state.test(v))
},
city: {
isValid: true
},
address1: {
isValid: true
},
address2: {
isValid: true
},
homePhone: {
isValid: (r, v) => (v === '' || regExpPattern.phone.test(v))
},
cellPhone: {
isValid: (r, v) => (v === '' || regExpPattern.phone.test(v))
},
dateOfBirth: {
isValid: true
},
dateOfHire: {
isValid: true
},
ssn: {
isValid: (r, v) => (v === '' || regExpPattern.ssn.test(v))
},
role: {
isDisabled: true
},
save: {
nonValue: true,
isDisabled: (r) => (!r.anyModified || !r.allValid)
}
}
constructor(props) {
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
this.state = {
validator: new Validator(
this.props.user, ProfileForm.validations, this.props.onModifiedChanged)
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.user !== this.props.user) {
this.setState({
validator: new Validator(
nextProps.user, ProfileForm.validations, nextProps.onModifiedChanged)
})
}
}
handleSubmit(e) {
e.preventDefault()
let obj = this.state.validator.getValues()
if (obj && this.props.onSaved) {
this.props.onSaved(obj)
}
}
render() {
return (
<Form className='profile-form' onSubmit={this.handleSubmit}>
<Grid stackable>
<Grid.Column width={3}>
<Image id='userImage' shape='circular' size='medium' src={this.props.userImageUrl} centered />
<FilePicker validExtensions={['.jpg', '.jpeg', '.png']} content='Select Image' onFileSelect={this.props.onSelectImage} />
</Grid.Column>
<Grid.Column width={13}>
<Form.Group>
<ValidatedInput label='First Name' name='firstName' width={8}
validator={this.state.validator} />
<ValidatedInput label='Last Name' name='lastName' width={8}
validator={this.state.validator} />
</Form.Group>
<Form.Group>
<ValidatedInput label='Email' name='email' width={8} message='Required. Must be a valid email address.'
validator={this.state.validator} />
<Form.Button fluid content={'Change Email'} label='&nbsp;'
width={4} onClick={this.props.onChangeEmail} />
<Form.Button fluid content={'Change Password'} label='&nbsp;'
width={4} onClick={this.props.onChangePassword} />
</Form.Group>
<Form.Group>
<ValidatedInput label='Zip' name='zip' width={4}
validator={this.state.validator} message='5 Character U.S. Zip Code. Optional.' />
<ValidatedDropdown label='State' name='state' width={6} message='Type or select a U.S. State or Province.'
placeholder='Select State' options={Constants.stateOptions}
validator={this.state.validator} searchable />
<ValidatedInput label='City' name='city' width={6}
validator={this.state.validator} message='U.S. City. Optional.' />
</Form.Group>
<Form.Group>
<ValidatedInput label='Address' name='address1' width={12}
validator={this.state.validator} message='Primary Street Address. Optional.' />
<ValidatedInput label='Apt. #' name='address2' width={4}
validator={this.state.validator} message='Apartment/Unit number. Optional.' />
</Form.Group>
<Form.Group>
<ValidatedInput label='Home Phone' name='homePhone' width={8}
validator={this.state.validator} message='A valid U.S. phone number. IE: (555)123-4567. Optional.' />
<ValidatedInput label='Cell Phone' name='cellPhone' width={8}
validator={this.state.validator} message='A valid U.S. phone number. IE: (555)123-4567. Optional.' />
</Form.Group>
<Form.Group>
<ValidatedDatePicker label='Date of Birth' name='dateOfBirth' width={5}
validator={this.state.validator} message='Select a date.' />
<ValidatedInput label='SSN' name='ssn' width={6}
validator={this.state.validator} message='U.S. Social Security Number. IE: 123-45-6789' />
<ValidatedDatePicker label='Hire Date' name='dateOfHire' width={5}
validator={this.state.validator} message='Select a date.' />
</Form.Group>
<Form.Group>
<Form.Field width={12} />
<ValidatedButton submit primary width={4} size='medium' content='Save' label='&nbsp;' name='save'
validator={this.state.validator} />
</Form.Group>
</Grid.Column>
</Grid>
</Form>
)
}
}

View File

@@ -0,0 +1,12 @@
.profile-form {
text-align: left;
margin: 3em auto 4em auto;
}
.profile-form > .fields {
margin-bottom: 1.5em !important;
}
#userImage {
margin-bottom: 2em;
}

View File

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

View File

@@ -0,0 +1,230 @@
import React from 'react'
import PropTypes from 'prop-types'
import { autoBind } from 'auto-bind2'
import { regExpPattern } from 'regexp-pattern'
import { Grid, Form } from 'semantic-ui-react'
import './UserForm.scss'
import { ValidatedEmailIcon } from './ValidatedEmailIcon'
import { Constants, api } from '../helpers'
import { Validator, ValidatedInput, ValidatedButton, ValidatedDropdown, ValidatedDatePicker, ValidatedContainer } from '../Validated'
export class UserForm extends React.Component {
static propTypes = {
user: PropTypes.object,
onSave: PropTypes.func,
onRemove: PropTypes.func,
onModifiedChanged: PropTypes.func,
onChangeEmail: PropTypes.func,
onResendEmail: PropTypes.func
}
static validations = {
email: {
isValid: (r, v) => (regExpPattern.email.test(v)),
isDisabled: (r) => (!!r._id)
},
emailValidated: {
isDisabled: (r) => (!!r._id === false)
},
changeEmail: {
nonValue: true,
isDisabled: (r) => (!!r._id === false)
},
firstName: {
isValid: (r, v) => (v !== '')
},
lastName: {
isValid: (r, v) => (v !== '')
},
zip: {
isValid: (r, v) => (v === '' || regExpPattern.zip.test(v))
},
state: {
isValid: (r, v) => (v === '' || regExpPattern.state.test(v))
},
city: {
isValid: true
},
address1: {
isValid: true
},
address2: {
isValid: true
},
homePhone: {
isValid: (r, v) => (v === '' || regExpPattern.phone.test(v))
},
cellPhone: {
isValid: (r, v) => (v === '' || regExpPattern.phone.test(v))
},
dateOfBirth: {
isValid: true
},
dateOfHire: {
isValid: true
},
ssn: {
isValid: (r, v) => (v === '' || regExpPattern.ssn.test(v))
},
numHouseholds: {
isValid: true
},
t12: {
isValid: true
},
aum: {
isValid: true
},
role: {
isValid: (r, v) => (v !== ''),
isDisabled: (r) => (api.loggedInUser._id === r._id)
},
project: {
isValid: (r, v) => (v !== '' || v === '')
},
remove: {
nonValue: true,
isVisible: (r) => (!!r._id),
isDisabled: (r) => (api.loggedInUser._id === r._id)
},
reset: {
nonValue: true,
isDisabled: (r) => (!r.anyModified)
},
submit: {
nonValue: true,
isDisabled: (r) => (!r.anyModified && !r.allValid)
},
'broker-fields': {
nonValue: true,
isVisible: (r, v) => (r.getField('role').value === 'broker')
},
'standard-fields': {
nonValue: true,
isVisible: (r, v) => (r.getField('role').value !== 'broker')
}
}
constructor(props) {
super(props)
autoBind(this, (name) => (name.startsWith('handle')))
this.state = {
validator: new Validator(this.props.user, UserForm.validations, this.props.onModifiedChanged)
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.user !== this.props.user) {
this.setState({
validator: new Validator(nextProps.user, UserForm.validations, nextProps.onModifiedChanged)
})
}
}
handleSubmit(e) {
e.preventDefault()
let obj = this.state.validator.getValues()
if (obj) {
this.props.onSave(obj)
}
}
handleReset() {
this.setState({ validator: new Validator(this.props.user, UserForm.validations, this.props.onModifiedChanged) })
this.props.onModifiedChanged(false)
}
handleChangeEmail() {
this.props.onChangeEmail()
}
handleResendEmail() {
this.props.onResendEmail()
}
render() {
return (
<Form className='user-form' onSubmit={this.handleSubmit}>
<Grid stackable>
<Grid.Column width={16}>
<Form.Group>
<ValidatedDropdown label={'Deighton Access & Security Level'} width={6} selection
options={Constants.accessLevels} name='role' message='The user role and security level'
placeholder='' validator={this.state.validator} />
</Form.Group>
<Form.Group>
<ValidatedInput label='First Name' name='firstName'
width={8} validator={this.state.validator} />
<ValidatedInput label='Last Name' name='lastName'
width={8} validator={this.state.validator} />
</Form.Group>
<Form.Group>
<ValidatedInput label='Email' name='email' width={8} message='Must be a valid email address. Required.'
validator={this.state.validator} />
<ValidatedEmailIcon name='emailValidated' validator={this.state.validator} width={4}
onClick={this.handleResendEmail} />
<ValidatedButton width={4} size='medium' content='Change Email' label='&nbsp;' name='changeEmail'
validator={this.state.validator} onClick={this.handleChangeEmail} />
</Form.Group>
<ValidatedContainer name='standard-fields' validator={this.state.validator}>
<Form.Group>
<ValidatedInput label='Zip' width={4} name='zip' message='5 Character U.S. Zip Code. Optional.'
position='bottom center' validator={this.state.validator} />
<ValidatedDropdown label='State' name='state' width={4}
placeholder='Select State' options={Constants.stateOptions} validator={this.state.validator} searchable />
<ValidatedInput label='City' width={8} type='text' name='city' message='U.S. City. Optional.'
validator={this.state.validator} />
</Form.Group>
<Form.Group>
<ValidatedInput label='Address' width={12} name='address1'
validator={this.state.validator} message='Primary Street Address. Optional.' />
<ValidatedInput label='Apt. #' width={4} name='address2'
validator={this.state.validator} message='Apartment/Unit number. Optional.' />
</Form.Group>
<Form.Group>
<ValidatedInput label='Home Phone' width={8} name='homePhone'
validator={this.state.validator} message='A valid U.S. phone number. IE: (555)123-4567. Optional.' />
<ValidatedInput label='Cell Phone' width={8} name='cellPhone'
validator={this.state.validator} message='A valid U.S. phone number. IE: (555)123-4567. Optional.' />
</Form.Group>
<Form.Group>
<ValidatedDatePicker label='Date of Birth' width={5} name='dateOfBirth'
validator={this.state.validator} />
<ValidatedInput label='SSN' width={6} name='ssn'
validator={this.state.validator} message='U.S. Social Security Number. IE: 123-45-6789' />
<ValidatedDatePicker label='Hire Date' width={5} name='dateOfHire'
validator={this.state.validator} />
</Form.Group>
</ValidatedContainer>
<ValidatedContainer name='broker-fields' validator={this.state.validator}>
<Form.Group>
<ValidatedInput label='# Households' width={5} name='numHouseholds'
message='Number of households in this brokers account' validator={this.state.validator} />
<ValidatedInput label='T-12' width={6} name='t12' message='This brokers T-12 info.'
validator={this.state.validator} />
<ValidatedInput label='AUM' width={5} name='aum'
message='This brokers AUM information.' validator={this.state.validator} />
</Form.Group>
</ValidatedContainer>
<Form.Group>
<ValidatedButton color='red' width={4} size='medium' content='Remove' label='&nbsp;' name='remove'
validator={this.state.validator} onClick={this.props.onRemove} />
<ValidatedButton width={4} size='medium' content='Reset' label='&nbsp;' name='reset'
validator={this.state.validator} onClick={this.handleReset} />
<Form.Field width={this.state.validator._id ? 8 : 12} />
<ValidatedButton primary submit width={4} size='medium'
content={this.state.validator._id ? 'Save' : 'Add'} label='&nbsp;' name='submit'
validator={this.state.validator} />
</Form.Group>
</Grid.Column>
</Grid>
</Form>
)
}
}

View File

@@ -0,0 +1,8 @@
.user-form {
text-align: left;
margin: 3em auto 4em auto;
}
.user-form > .fields {
margin-bottom: 1.5em !important;
}

View File

@@ -0,0 +1,9 @@
import React from 'react'
import './UserFormPlaceholder.scss'
export const UserFormPlaceholder = () => (
<div className='user-form-placeholder'>
<h3>Select a registered user to view details here</h3>
<h5>Or 'Add New User'</h5>
</div>
)

View File

@@ -0,0 +1,7 @@
.user-form-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}

View File

@@ -0,0 +1,69 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Dropdown, List, Icon, Button, Image } from 'semantic-ui-react'
import { Constants, api } from '../helpers'
import './UserList.scss'
const selectionOptions = Constants.accessLevels.concat([{ value: 'all', text: 'All Levels' }])
export class UserList extends React.Component {
static propTypes = {
users: PropTypes.array,
onUserListClick: PropTypes.func,
selectedUser: PropTypes.object,
selectionModified: PropTypes.bool,
onAddNewUser: PropTypes.func
}
constructor(props) {
super(props)
this.state = {
users: null
}
this.filterUsers = this.filterUsers.bind(this)
}
componentWillReceiveProps(nextProps) {
// This ensures that the original list is populated with 'all' by default,
// due to the component mounting before users are returned (by api)
if (nextProps.users !== this.props.users) {
this.setState({ users: nextProps.users })
}
}
filterUsers(e, data) {
e.preventDefault()
let users = this.props.users
if (data.value !== 'all') {
users = users.filter(user => data.value === user.role)
}
this.setState({ users })
}
render() {
return (
<div>
<Dropdown placeholder='All Registered Users' selection fluid
options={selectionOptions} onChange={this.filterUsers} />
<List className='user-list' size='big' selection verticalAlign='middle'>
{
this.state.users
? this.state.users.map((user, index) =>
(<List.Item className='user-list-item' key={user._id || '0'} onClick={this.props.onUserListClick}
active={user === this.props.selectedUser} data-index={index}>
<Image avatar src={api.makeImageUrl(user.thumbnailImageId, Constants.smallUserImageSize)} />
<List.Content>
{ user._id ? user.firstName + ' ' + user.lastName : '[New User]' }
{ user === this.props.selectedUser && this.props.selectionModified ? <Icon className='user-update'
name='edit' /> : null }
</List.Content>
</List.Item>)
)
: null
}
</List>
<Button className='add-new-user' content='Add New User' primary onClick={this.props.onAddNewUser} />
</div>
)
}
}

View File

@@ -0,0 +1,21 @@
.user-list {
text-align: left;
width: 100%;
height: 100%;
padding: 0.25em !important;
border-radius: 5px;
box-shadow: 0 0 0.25em 2px Silver;
-webkit-box-shadow: 0 0 0.25em 2px Silver;
max-height: 60vh;
min-height: 60vh;
overflow-y: scroll;
}
.user-list > .item {
margin: 5em 0;
}
i.user-update {
position: absolute;
right: 7.5%;
}

281
website/src/Users/Users.js Normal file
View File

@@ -0,0 +1,281 @@
import React from 'react'
import { Container, Grid } from 'semantic-ui-react'
import { autoBind } from 'auto-bind2'
import { UserList } from './UserList'
import { UserForm } from './UserForm'
import { UserFormPlaceholder } from './UserFormPlaceholder'
import { api } from '../helpers'
import { YesNoMessageDialog, MessageDialog, ChangeEmailDialog, WaitDialog } from '../Dialog'
export class Users extends React.Component {
constructor() {
super()
autoBind(this, (name) => (name.startsWith('handle')))
this.state = {
selectedUser: null,
users: [],
yesNoDialog: null,
messageDialog: null,
waitDialog: null,
changeEmailDialog: null
}
}
componentDidMount() {
api.listUsers().then((list) => {
list.items.sort((userA, userB) => (userA.lastName.localeCompare(userB.lastName)))
this.setState({ users: list.items })
}).catch((error) => {
this.setState({
messageDialog: {
error: true,
title: 'User List Error',
message: `Unable to get the list of users. ${error.message}`
}
})
})
}
removeUnfinishedNewUser() {
let users = this.state.users
if (users.length > 0 && !users[0]._id) {
this.setState({ users: this.state.users.slice(1) })
}
}
handleUserListClick(e) {
let user = this.state.users[Number(e.currentTarget.getAttribute('data-index'))]
if (this.state.modified) {
this.nextSelectedUser = user
this.setState({
yesNoDialog: {
title: 'User Modified',
message: 'This user has been modified. Are you sure you would like to navigate away?',
onDismiss: this.handleModifiedDialogDismiss
}
})
} else {
this.setState({ selectedUser: user })
this.removeUnfinishedNewUser()
}
}
handleSave(user) {
if (user._id) {
this.setState({ waitDialog: { message: 'Updating User' } })
api.updateUser(user).then((updatedUser) => {
this.setState({
waitDialog: null,
users: this.state.users.map((user) => (user._id === updatedUser._id ? updatedUser : user)),
modified: false,
selectedUser: updatedUser
})
}).catch((error) => {
this.setState({
waitDialog: null,
messageDialog: {
error: true,
title: 'Update Error',
message: `Unable to save the user changes. ${error.message}`
}
})
})
} else {
this.setState({ waitDialog: { message: 'Creating User' } })
api.createUser(user).then((createdUser) => {
this.setState({
waitDialog: false,
users: this.state.users.map((user) => (!user._id ? createdUser : user)),
modified: false,
selectedUser: createdUser
})
}).catch((error) => {
this.setState({
waitDialog: null,
messageDialog: {
error: true,
title: 'Create Error',
message: `Unable to create the user. ${error.message}`
}
})
})
}
}
handleChangeEmail() {
this.setState({ changeEmailDialog: {} })
}
handleResendEmail() {
this.setState({
waitDialog: { message: 'Resending Email...' }
})
api.sendConfirmEmail({ existingEmail: this.state.selectedUser.email }).then(() => {
this.setState({
waitDialog: null,
messageDialog: {
error: false,
title: "We've Re-Sent the Email...",
message: `An email has been sent to '${this.state.selectedUser.email}'. This user will need to follow that email's included link to verify their account.`
}
})
}).catch((error) => {
this.setState({
error: true,
waitDialog: null,
messageDialog: {
error: true,
title: 'Email Change Error...',
message: `Unable to request email change. ${error ? error.message : ''}`
}
})
})
}
handleChangeEmailDismiss(newEmail) {
this.setState({ changeEmailDialog: null })
if (!newEmail) {
return
}
this.setState({
waitDialog: { message: 'Requesting Email Change...' }
})
api.sendConfirmEmail({ existingEmail: this.state.selectedUser.email, newEmail }).then(() => {
this.setState({
waitDialog: null,
messageDialog: {
error: false,
title: 'Email Change Requested...',
message: `An email has been sent to '${newEmail}'. This user will need to follow that email's included link to finish changing their email.`
}
})
}).catch((error) => {
this.setState({
error: true,
waitDialog: null,
messageDialog: {
error: true,
title: 'Email Change Error...',
message: `Unable to request email change. ${error ? error.message : ''}`
}
})
})
}
handleRemove() {
this.setState({
yesNoDialog: {
title: 'Permanently Delete User?',
message: 'You are about to delete this user from the system. This includes references to this user in Projects, Packages, and so on. Are you sure you want to remove this user?',
onDismiss: this.handleRemoveDialogDismiss
}
})
}
handleRemoveDialogDismiss(yes) {
if (yes) {
// TODO: Pass the _id back from the dialog input data
const selectedUserId = this.state.selectedUser._id
const selectedIndex = this.state.users.findIndex((user) => (user._id === selectedUserId))
if (selectedIndex >= 0) {
this.setState({ waitDialog: { message: 'Removing User' } })
api.deleteUser(selectedUserId).then(() => {
this.setState({
waitDialog: null,
users: [...this.state.users.slice(0, selectedIndex), ...this.state.users.slice(selectedIndex + 1)],
selectedUser: null
})
}).catch((error) => {
this.setState({
waitDialog: null,
messageDialog: {
error: true,
title: 'Remove Error',
message: `Unable to remove the user. ${error.message}`
}
})
})
}
}
this.setState({
yesNoDialog: null
})
}
handleModifiedDialogDismiss(yes) {
if (yes) {
this.setState({
selectedUser: this.nextSelectedUser,
modified: false
})
this.removeUnfinishedNewUser()
delete this.nextSelectedUser
}
this.setState({
yesNoDialog: null
})
}
handleMessageDialogDismiss() {
this.setState({ messageDialog: null })
}
handleModifiedChanged(modified) {
this.setState({ modified: modified })
}
handleAddNewUser() {
let newUser = {}
let newUsers = [newUser].concat(this.state.users)
this.setState({ users: newUsers, selectedUser: newUser })
}
render() {
return (
<Container>
<div>Users</div>
<Grid stackable>
{/* User List - Displayed on left hand side. */}
<Grid.Column width={5}>
<UserList users={this.state.users} selectedUser={this.state.selectedUser}
selectionModified={this.state.modified} onUserListClick={this.handleUserListClick}
onAddNewUser={this.handleAddNewUser} />
</Grid.Column>
{/* User Info - Displayed on right hand side. */}
<Grid.Column width={11}>
{
this.state.selectedUser
? <UserForm user={this.state.selectedUser} onSave={this.handleSave}
onRemove={this.handleRemove} onModifiedChanged={this.handleModifiedChanged}
onChangeEmail={this.handleChangeEmail} onResendEmail={this.handleResendEmail} />
: <UserFormPlaceholder />
}
</Grid.Column>
</Grid>
<ChangeEmailDialog open={!!this.state.changeEmailDialog} onDismiss={this.handleChangeEmailDismiss} />
<YesNoMessageDialog open={!!this.state.yesNoDialog}
title={this.state.yesNoDialog ? this.state.yesNoDialog.title : ''}
message={this.state.yesNoDialog ? this.state.yesNoDialog.message : ''}
onDismiss={this.state.yesNoDialog ? this.state.yesNoDialog.onDismiss : null} />
<MessageDialog
open={!!this.state.messageDialog}
error={this.state.messageDialog ? this.state.messageDialog.error : false}
title={this.state.messageDialog ? this.state.messageDialog.title : ''}
message={this.state.messageDialog ? this.state.messageDialog.message : ''}
onDismiss={this.handleMessageDialogDismiss} />
<WaitDialog active={!!this.state.waitDialog} message={this.state.waitDialog ? this.state.waitDialog.message : ''} />
</Container>
)
}
}

View File

@@ -0,0 +1,47 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Icon, Popup, Button } from 'semantic-ui-react'
import './ValidatedEmailIcon.scss'
// This is a validated component with a value that cannot change itself and is specialized
export class ValidatedEmailIcon extends React.Component {
static propTypes = {
name: PropTypes.string,
validator: PropTypes.object,
width: PropTypes.number,
onClick: PropTypes.func
}
constructor(props) {
super(props)
this.state = props.validator.getField('emailValidated')
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
if (this.state.value) {
return (
<Form.Field width={this.props.width}>
<label>&nbsp;</label>
<Popup content='Email Validated' position='bottom center' hoverable trigger={
<Icon name='mail' color='green' size='big' className='mail-validated-icon' />
} />
</Form.Field>
)
} else {
return (
<Form.Field width={this.props.width}>
<label>&nbsp;</label>
<Button fluid icon='mail outline' color='red' labelPosition='left'
content='Resend Email' onClick={this.props.onClick} disabled={this.state.disabled} />
</Form.Field>
)
}
}
}

View File

@@ -0,0 +1,3 @@
.mail-validated-icon {
padding-top: 4px;
}

View File

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

View File

@@ -0,0 +1,59 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Button } from 'semantic-ui-react'
export class ValidatedActionsButton extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
size: PropTypes.string,
validator: PropTypes.object.isRequired,
primary: PropTypes.bool,
submit: PropTypes.bool,
color: PropTypes.string,
form: PropTypes.string,
children: PropTypes.node
}
constructor(props) {
super(props)
this.updateValue = this.updateValue.bind(this)
let { name, validator } = this.props
validator.addListener(name, this.updateValue)
this.state = validator.getField(name)
}
updateValue(name) {
this.setState(this.props.validator.getField(name))
}
componentWillUnmount() {
this.props.validator.removeListener(this.props.name, this.updateValue)
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.props.validator.removeListener(this.props.name, this.updateValue)
nextProps.validator.addListener(nextProps.name, this.updateValue)
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
const { children, size, color, primary, submit, name, form } = this.props
if (this.state.visible) {
return (
<Button color={color} primary={primary}
disabled={this.state.disabled}
type={submit ? 'submit' : 'button'}
size={size} name={name} form={form}>
{children}
</Button>
)
} else {
return null
}
}
}

View File

@@ -0,0 +1,61 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Button } from 'semantic-ui-react'
export class ValidatedButton extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
width: PropTypes.number,
size: PropTypes.string,
content: PropTypes.string,
validator: PropTypes.object.isRequired,
primary: PropTypes.bool,
submit: PropTypes.bool,
color: PropTypes.string,
floated: PropTypes.string,
onClick: PropTypes.func
}
constructor(props) {
super(props)
this.updateValue = this.updateValue.bind(this)
let { name, validator } = this.props
validator.addListener(name, this.updateValue)
this.state = validator.getField(name)
}
updateValue(name) {
this.setState(this.props.validator.getField(name))
}
componentWillUnmount() {
this.props.validator.removeListener(this.props.name, this.updateValue)
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.props.validator.removeListener(this.props.name, this.updateValue)
nextProps.validator.addListener(nextProps.name, this.updateValue)
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
if (this.state.visible) {
return (
<Form.Field width={this.props.width} disabled={this.state.disabled}>
<label>{this.props.label}</label>
<Button fluid color={this.props.color} primary={this.props.primary}
type={this.props.submit ? 'submit' : 'button'} onClick={this.props.onClick}
content={this.props.content} size={this.props.size} name={this.props.name}
floated={this.props.floated} />
</Form.Field>
)
} else {
return null
}
}
}

View File

@@ -0,0 +1,48 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Popup, Checkbox } from 'semantic-ui-react'
// This is an example of a validated component with a value that can change itself, that cannot ever be invalid.
export class ValidatedCheckbox extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
label: PropTypes.string,
width: PropTypes.number,
validator: PropTypes.object.isRequired,
className: PropTypes.string
}
constructor(props) {
super(props)
this.state = props.validator.getField(props.name)
this.handleChange = this.handleChange.bind(this)
}
handleChange(e, data) {
const { validator, name } = this.props
const state = validator.getField(name)
if (!state.readOnly && !state.disabled) {
this.setState(validator.updateValue(name, data.checked))
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
return (
<Form.Field width={this.props.width} disabled={this.state.disabled} className={this.props.className}>
<Popup content={this.props.message} position='bottom center' hoverable trigger={
<Checkbox checked={!!this.state.value} name={this.props.name} label={this.props.label}
onChange={this.handleChange} />} />
</Form.Field>
)
}
}

View File

@@ -0,0 +1,52 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Container } from 'semantic-ui-react'
export class ValidatedContainer extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
validator: PropTypes.object.isRequired,
children: PropTypes.oneOfType([
PropTypes.array,
PropTypes.object
])
}
constructor(props) {
super(props)
this.updateValue = this.updateValue.bind(this)
let { name, validator } = this.props
validator.addListener(name, this.updateValue)
this.state = validator.getField(name)
}
updateValue(name) {
this.setState(this.props.validator.getField(name))
}
componentWillUnmount() {
this.props.validator.removeListener(this.props.name, this.updateValue)
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.props.validator.removeListener(this.props.name, this.updateValue)
nextProps.validator.addListener(nextProps.name, this.updateValue)
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
if (this.state.visible) {
return (
<Container>
{this.props.children}
</Container>
)
} else {
return null
}
}
}

View File

@@ -0,0 +1,56 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Popup } from 'semantic-ui-react'
import 'moment'
import { DatePickerInput } from 'rc-datepicker'
import 'rc-datepicker/lib/style.css'
export class ValidatedDatePicker extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
message: PropTypes.string.isRequired,
label: PropTypes.string,
width: PropTypes.number,
validator: PropTypes.object.isRequired
}
constructor(props) {
super(props)
this.state = props.validator.getField(props.name)
this.handleChange = this.handleChange.bind(this)
this.handleClear = this.handleClear.bind(this)
}
handleChange(e, data) {
const { validator, name } = this.props
const state = validator.getField(name)
if (!state.readOnly && !state.disabled) {
// NOTE: data is a little different for this control - no value property
this.setState(validator.updateValue(name, data))
}
}
handleClear(e) {
this.props.validator.updateValue(this.props.name, '')
this.setState(this.props.validator.getField(this.props.name))
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
return (
<Form.Field error={!this.state.valid} width={this.props.width} disabled={this.state.disabled}>
<label>{this.props.label}</label>
<Popup content={this.props.message} position='right center' hoverable trigger={
<DatePickerInput value={this.state.value} name={this.props.name} onChange={this.handleChange}
showOnInputClick onClear={this.handleClear} />} />
</Form.Field>
)
}
}

View File

@@ -0,0 +1,47 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Dropdown, Popup } from 'semantic-ui-react'
export class ValidatedDropdown extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
width: PropTypes.number,
placeholder: PropTypes.string,
options: PropTypes.array,
message: PropTypes.string.isRequired,
validator: PropTypes.object.isRequired,
searchable: PropTypes.bool
}
constructor(props) {
super(props)
this.state = props.validator.getField(props.name)
this.handleChange = this.handleChange.bind(this)
}
handleChange(e, data) {
const { validator, name } = this.props
this.setState(validator.updateValue(name, data.value))
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
return (
<Form.Field error={!this.state.valid} width={this.props.width}>
<label>{this.props.label}</label>
<Popup content={this.props.message} position='right center' hoverable trigger={
<Dropdown selection fluid disabled={this.state.disabled} readOnly={this.state.readOnly}
placeholder={this.props.placeholder} options={this.props.options}
value={this.state.value} onChange={this.handleChange} search={this.props.searchable} />} />
</Form.Field>
)
}
}

View File

@@ -0,0 +1,63 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Form, Popup, Input } from 'semantic-ui-react'
// This is an example of a validated component with a value that changes itself
export class ValidatedInput extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
message: PropTypes.string,
label: PropTypes.string,
width: PropTypes.number,
validator: PropTypes.object.isRequired,
password: PropTypes.bool,
className: PropTypes.string,
placeholder: PropTypes.string,
icon: PropTypes.string,
iconPosition: PropTypes.string
}
constructor(props) {
super(props)
this.state = props.validator.getField(props.name)
this.handleChange = this.handleChange.bind(this)
}
handleChange(e, data) {
const { validator, name } = this.props
const state = validator.getField(name)
if (!state.readOnly && !state.disabled) {
this.setState(validator.updateValue(name, data.value))
}
}
componentWillReceiveProps(nextProps) {
if (nextProps.validator !== this.props.validator) {
this.setState(nextProps.validator.getField(nextProps.name))
}
}
render() {
return (
<Form.Field error={!this.state.valid} width={this.props.width} disabled={this.state.disabled} className={this.props.className}>
<label>{this.props.label}</label>
{this.props.message
? <Popup content={this.props.message} position='bottom center' hoverable trigger={
<Input value={this.state.value} type={this.props.password ? 'password' : 'text'}
name={this.props.name} onChange={this.handleChange} placeholder={this.props.placeholder}
className={this.props.className} icon={this.props.icon} iconPosition={this.props.iconPosition} />}
/>
: <Input value={this.state.value}
type={this.props.password ? 'password' : 'text'}
name={this.props.name} onChange={this.handleChange}
placeholder={this.props.placeholder}
className={this.props.className} icon={this.props.icon}
iconPosition={this.props.iconPosition} />
}
</Form.Field>
)
}
}

View File

@@ -0,0 +1,188 @@
import EventEmitter from 'eventemitter3'
const any = function(obj, pred) {
for (let name in obj) {
if (pred(obj[name])) {
return true
}
}
return false
}
export class Validator extends EventEmitter {
constructor(originalObj, validations, onModifiedChanged) {
super()
this.updateValue = this.updateValue.bind(this)
this._id = originalObj._id
this.onModifiedChanged = onModifiedChanged
this.fields = {}
this.nonValueFields = {}
for (let name in validations) {
let validation = validations[name]
let field = {
isDisabled: this.normalize(validation.isDisabled, false),
isReadOnly: this.normalize(validation.isReadOnly, false),
isVisible: this.normalize(validation.isVisible, true),
nonValue: validation.nonValue || false
}
if (field.nonValue) {
field.disabled = field.isDisabled(this)
field.readOnly = field.isReadOnly(this)
field.visible = field.isVisible(this)
this.nonValueFields[name] = field
} else {
field.alwaysGet = validation.alwaysGet
field.isValid = this.normalize(validation.isValid, true)
field.initValue = (validation.initValue === undefined ? '' : validation.initValue)
field.originalValue = Validator.getObjectValue(originalObj, name)
this.updateFieldValue(field, field.originalValue || field.initValue)
this.fields[name] = field
}
}
this.updateNonValueFields()
}
normalize(obj, def) {
return obj ? ((obj.constructor === Function) ? obj : () => (!!obj)) : () => (def)
}
static stateSafeField(field) {
return {
value: field.value,
modified: field.modified,
valid: field.valid,
disabled: field.disabled,
readOnly: field.readOnly,
visible: field.visible
}
}
updateValue(name, newValue) {
let lastAnyModified = this.anyModified
let field = this.fields[name]
if (field) {
this.updateFieldValue(field, newValue)
this.updateNonValueFields()
if (lastAnyModified !== this.anyModified && this.onModifiedChanged) {
this.onModifiedChanged(this.anyModified)
}
}
return Validator.stateSafeField(field)
}
updateFieldValue(field, newValue) {
field.value = newValue
field.disabled = field.isDisabled(this)
field.readOnly = field.isReadOnly(this)
field.visible = field.isVisible(this)
field.valid = field.isValid(this, newValue)
field.modified =
field.originalValue !== undefined ? (field.originalValue !== newValue) : (newValue !== field.initValue)
this.anyModified = field.modified || any(this.fields, (field) => (field.modified))
this.allValid = !field.valid ? false : !any(this.fields, (field) => (!field.valid))
}
updateNonValueFields() {
for (let name in this.nonValueFields) {
let field = this.nonValueFields[name]
let disabled = field.isDisabled(this)
let readOnly = field.isReadOnly(this)
let visible = field.isVisible(this)
let b = (disabled !== field.disabled || readOnly !== field.readOnly || visible !== field.visible)
field.disabled = disabled
field.readOnly = readOnly
field.visible = visible
if (b) {
this.emit(name, name)
}
}
}
getField(name) {
let field = this.fields[name] || this.nonValueFields[name]
if (!field) {
throw new Error(`Field '${name}' does not have a validation entry`)
}
return Validator.stateSafeField(field)
}
getValues() {
// Generate an object that has the modified and alwaysGet fields
let obj = {}
if (!this.anyModified && !this.allValid) {
return obj
}
// Will have an _id if updating
if (this._id) {
obj._id = this._id
}
for (let name in this.fields) {
let field = this.fields[name]
if (field.alwaysGet || (!field.nonValue && field.modified)) {
let value = field.value
if (value && value.constructor === 'String') {
value = value.trim()
}
Validator.setObjectValue(obj, name, value)
}
}
return obj
}
getOriginalValues() {
// Generate an object that has the original values of all fields
let obj = {}
if (this._id) {
obj._id = this._id
}
for (let name in this.fields) {
let field = this.fields[name]
if (field.originalValue !== undefined) {
Validator.setObjectValue(obj, name, field.originalValue)
}
}
return obj
}
static getObjectValue(obj, name) {
name.split('.').forEach((namePart) => {
if (obj) {
obj = obj[namePart]
}
})
return obj
}
static setObjectValue(obj, name, value) {
name.split('.').forEach((namePart, i, nameParts) => {
if (i < nameParts.length - 1) {
if (!obj[namePart]) {
obj[namePart] = {}
}
obj = obj[namePart]
} else {
obj[namePart] = value
}
})
}
}

View File

@@ -0,0 +1,8 @@
export { Validator } from './Validator'
export { ValidatedInput } from './ValidatedInput'
export { ValidatedButton } from './ValidatedButton'
export { ValidatedActionsButton } from './ValidatedActionsButton'
export { ValidatedDropdown } from './ValidatedDropdown'
export { ValidatedDatePicker } from './ValidatedDatePicker'
export { ValidatedCheckbox } from './ValidatedCheckbox'
export { ValidatedContainer } from './ValidatedContainer'

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

426
website/src/helpers/Api.js Normal file
View File

@@ -0,0 +1,426 @@
import EventEmitter from 'eventemitter3'
import io from 'socket.io-client'
const authToken = 'deightonAuthToken'
class NetworkError extends Error {
constructor(message) {
super(message)
this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = (new Error(message)).stack
}
}
}
class ApiError extends Error {
constructor(status, message) {
super(message || '')
this.status = status || 500
this.name = this.constructor.name
if (typeof Error.captureStackTrace === 'function') {
Error.captureStackTrace(this, this.constructor)
} else {
this.stack = (new Error(message)).stack
}
}
}
class Api extends EventEmitter {
constructor() {
super()
this.user = null
let token = localStorage.getItem(authToken) || sessionStorage.getItem(authToken)
if (token) {
this.token = token
this.user = { pending: true }
this.who()
.then((user) => {
this.user = user
this.connectSocket()
this.emit('login')
})
.catch(() => {
localStorage.removeItem(authToken)
sessionStorage.removeItem(authToken)
this.token = null
this.user = null
this.socket = null
this.emit('logout')
})
}
}
connectSocket() {
this.socket = io(window.location.origin, {
path: '/api/socketio',
query: {
auth_token: this.token
}
})
this.socket.on('disconnect', (reason) => {
// Could happen if the auth_token is bad
this.socket = null
})
this.socket.on('notify', (message) => {
const { eventName, eventData } = message
// Filter the few massages that affect our cached user data to avoid a server round trip
switch (eventName) {
case 'newThumbnailImage':
this.user.thumbnailImageId = eventData.imageId
break
case 'newProfileImage':
this.user.imageId = eventData.imageId
break
default:
// Nothing to see here...
break
}
this.emit(message.eventName, message.eventData)
})
}
disconnectSocket() {
if (this.socket) {
this.socket.disconnect()
this.socket = null
}
}
get loggedInUser() {
return this.user
}
makeImageUrl(id, size) {
if (id) {
return '/api/assets/' + id + '?access_token=' + this.token
} else if (size && size.width && size.height) {
return `/api/placeholders/${size.width}x${size.height}?access_token=${this.token}`
} else {
return null
}
}
makeAssetUrl(id) {
return id ? '/api/assets/' + id + '?access_token=' + this.token : null
}
static makeParams(params) {
return params ? '?' + Object.keys(params).map((key) => (
[key, params[key]].map(encodeURIComponent).join('=')
)).join('&') : ''
}
request(method, path, requestBody, requestOptions) {
requestOptions = requestOptions || {}
var promise = new Promise((resolve, reject) => {
let fetchOptions = {
method: method,
mode: 'cors',
cache: 'no-store'
}
let headers = new Headers()
if (this.token) {
headers.set('Authorization', 'Bearer ' + this.token)
}
if (method === 'POST' || method === 'PUT') {
if (requestOptions.binary) {
headers.set('Content-Type', 'application/octet-stream')
headers.set('Content-Length', requestOptions.binary.length)
headers.set('Range', 'byte ' + requestOptions.binary.offset)
fetchOptions.body = requestBody
} else {
headers.set('Content-Type', 'application/json')
fetchOptions.body = JSON.stringify(requestBody)
}
}
fetchOptions.headers = headers
fetch('/api' + path, fetchOptions).then((res) => {
return Promise.all([ Promise.resolve(res), (requestOptions.binary && method === 'GET') ? res.blob() : res.json() ])
}).then((arr) => {
let [ res, responseBody ] = arr
if (res.ok) {
if (requestOptions.wantHeaders) {
resolve({ body: responseBody, headers: res.headers })
} else {
resolve(responseBody)
}
} else {
reject(new ApiError(res.status, responseBody.message))
}
}).catch((error) => {
reject(new NetworkError(error.message))
})
})
return promise
}
post(path, requestBody, options) {
return this.request('POST', path, requestBody, options)
}
put(path, requestBody, options) {
return this.request('PUT', path, requestBody, options)
}
get(path, options) {
return this.request('GET', path, options)
}
delete(path, options) {
return this.request('DELETE', path, options)
}
login(email, password, remember) {
return new Promise((resolve, reject) => {
this.post('/auth/login', { email, password }, { wantHeaders: true }).then((response) => {
// Save bearer token for later use
const authValue = response.headers.get('Authorization')
const [ scheme, token ] = authValue.split(' ')
if (scheme !== 'Bearer' || !token) {
reject(new ApiError('Unexpected Authorization scheme or token'))
}
if (remember) {
localStorage.setItem(authToken, token)
} else {
sessionStorage.setItem(authToken, token)
}
this.token = token
this.user = response.body
this.connectSocket()
this.emit('login')
resolve(response.body)
}).catch((err) => {
reject(err)
})
})
}
logout() {
let cb = () => {
// Regardless of response, always logout in the client
localStorage.removeItem(authToken)
sessionStorage.removeItem(authToken)
this.token = null
this.user = null
this.disconnectSocket()
this.emit('logout')
}
return this.delete('/auth/login').then(cb, cb)
}
who() {
return this.get('/auth/who')
}
confirmEmail(emailToken) {
return this.post('/auth/email/confirm/', { emailToken })
}
sendConfirmEmail(emails) {
return this.post('/auth/email/send', emails)
}
changePassword(passwords) {
return this.post('/auth/password/change', passwords)
}
sendResetPassword(email) {
return this.post('/auth/password/send', { email })
}
resetPassword(passwords) {
return this.post('/auth/password/reset', passwords)
}
getUser(_id) {
return this.get('/users/' + _id)
}
listUsers() {
return this.get('/users')
}
listBrokerUsers() {
return this.get('/users/brokers')
}
createUser(user) {
return this.post('/users', user)
}
updateUser(user) {
return new Promise((resolve, reject) => {
this.put('/users', user).then((user) => {
// If we just updated ourselves, update the internal cached copy
if (user._id === this.user._id) {
this.user = user
this.emit('login')
}
resolve(user)
}).catch((reason) => {
reject(reason)
})
})
}
deleteUser(_id) {
return this.delete('/users/' + _id)
}
setUserImage(details) {
return this.put('/users/set-image', details)
}
enterRoom(roomName) {
return this.put('/users/enter-room/' + (roomName || ''))
}
leaveRoom() {
return this.put('/users/leave-room')
}
getItemsForPathname(pathname) {
return this.get('/fingerprint' + pathname)
}
getCorporation(_id, params) {
return this.get('/corporations/' + _id + Api.makeParams(params))
}
listCorporations(params) {
return this.get('/corporations' + Api.makeParams(params))
}
createCorporation(corporation) {
return this.post('/corporations', corporation)
}
updateCorporation(corporation) {
return this.put('/corporations', corporation)
}
deleteCorporation(_id) {
return this.delete('/corporations/' + _id)
}
setCorporationImage(details) {
// _id: corporation id
// imageId: image asset id
// size: desired size of image { width, height }
return this.put('/corporations/set-image', details)
}
getBranch(_id, params) {
return this.get('/branches/' + _id + Api.makeParams(params))
}
listBranches(params) {
return this.get('/branches' + Api.makeParams(params))
}
createBranch(branch) {
return this.post('/branches', branch)
}
updateBranch(branch) {
return this.put('/branches', branch)
}
deleteBranch(_id) {
return this.delete('/branches/' + _id)
}
getProject(projectId, params) {
return this.get('/projects/' + projectId)
}
getPopulatedProject(projectId, params) {
return this.get('/projects/' + projectId + '/populated')
}
getProjectBrokerClientData(projectId, brokerId) {
return this.get('/projects/' + projectId + '/broker/' + brokerId)
}
signOffProjectBrokerClientData(projectId, brokerId) {
return this.post('/projects/' + projectId + '/broker/' + brokerId + '/sign-off', {})
}
createProjectPackages(projectId) {
return this.post('/projects/' + projectId + '/create-packages', {})
}
resetProjectPackages(projectId) {
return this.post('/projects/' + projectId + '/reset-packages', {})
}
buildProjectPDFs(projectId) {
return this.post('/projects/' + projectId + '/build-pdfs', {})
}
listProjects(params) {
return this.get('/projects' + Api.makeParams(params))
}
listDashboardProjects() {
return this.get('/projects/dashboard')
}
listBrokerProjects(brokerId) {
return this.get('/projects/broker/' + brokerId)
}
createProject(project) {
return this.post('/projects', project)
}
importProjectClientData(importData) {
return this.post('/projects/import-client-data', importData)
}
updateProject(project) {
return this.put('/projects', project)
}
deleteProject(_id) {
return this.delete('/projects/' + _id)
}
getPackage(_id, params) {
return this.get('/packages/' + _id + Api.makeParams(params))
}
listPackages(params) { // Example: listPackages({ projectId: '59c2faa32d27b9d10bd764b3' })
return this.get('/packages' + Api.makeParams(params))
}
getFormSet(formSetId) {
return this.get('/formsets/' + formSetId)
}
getForm(formSetId, formId) {
return this.get('/formsets/' + formSetId + '/form/' + formId)
}
listFormSets(params) {
return this.get('/formsets' + Api.makeParams(params))
}
setFormPDF(formSetId, formId, pdfAssetId) {
return this.post('/formsets/' + formSetId + '/form/' + formId, { pdfAssetId })
}
upload(file, progressCallback) {
return new Promise((resolve, reject) => {
const chunkSize = 32 * 1024
let reader = new FileReader()
const fileSize = file.size
const numberOfChunks = Math.ceil(fileSize / chunkSize)
let chunk = 0
let uploadId = null
reader.onload = (e) => {
const buffer = e.target.result
const bytesRead = buffer.byteLength
this.post('/assets/upload/' + uploadId, buffer, {
binary: { offset: chunk * chunkSize, length: bytesRead }
}).then((uploadData) => {
chunk++
if (!progressCallback(uploadData)) {
return Promise.reject(new Error('Upload was canceled'))
}
if (chunk < numberOfChunks) {
let start = chunk * chunkSize
let end = Math.min(fileSize, start + chunkSize)
reader.readAsArrayBuffer(file.slice(start, end))
} else {
resolve(uploadData)
}
}).catch((err) => {
reject(err)
})
}
this.post('/assets/upload', {
fileName: file.name,
fileSize,
contentType: file.type,
numberOfChunks
}).then((uploadData) => {
uploadId = uploadData.uploadId
reader.readAsArrayBuffer(file.slice(0, chunkSize))
}).catch((err) => {
reject(err)
})
})
}
}
export let api = new Api()

View File

@@ -0,0 +1,75 @@
export class Constants {
static stateOptions = [
{ value: '', text: '' },
{ value: 'AL', text: 'Alabama' },
{ value: 'AK', text: 'Alaska' },
{ value: 'AS', text: 'American Samoa' },
{ value: 'AZ', text: 'Arizona' },
{ value: 'AR', text: 'Arkansas' },
{ value: 'CA', text: 'California' },
{ value: 'CO', text: 'Colorado' },
{ value: 'CT', text: 'Connecticut' },
{ value: 'DE', text: 'Delaware' },
{ value: 'DC', text: 'District of Columbia' },
{ value: 'FM', text: 'Federated States of Micronesia' },
{ value: 'FL', text: 'Florida' },
{ value: 'GA', text: 'Georgia' },
{ value: 'GU', text: 'Guam' },
{ value: 'HI', text: 'Hawaii' },
{ value: 'ID', text: 'Idaho' },
{ value: 'IL', text: 'Illinois' },
{ value: 'IN', text: 'Indiana' },
{ value: 'IA', text: 'Iowa' },
{ value: 'KS', text: 'Kansas' },
{ value: 'KY', text: 'Kentucky' },
{ value: 'LA', text: 'Louisiana' },
{ value: 'ME', text: 'Maine' },
{ value: 'MH', text: 'Marshall Islands' },
{ value: 'MD', text: 'Maryland' },
{ value: 'MA', text: 'Massachusetts' },
{ value: 'MI', text: 'Michigan' },
{ value: 'MN', text: 'Minnesota' },
{ value: 'MS', text: 'Missippi' },
{ value: 'MO', text: 'Missouri' },
{ value: 'MT', text: 'Montana' },
{ value: 'NE', text: 'Nebraska' },
{ value: 'NV', text: 'Nevada' },
{ value: 'NH', text: 'New Hampshire' },
{ value: 'NJ', text: 'New Jersey' },
{ value: 'NM', text: 'New Mexico' },
{ value: 'NY', text: 'New York' },
{ value: 'NC', text: 'North Carolina' },
{ value: 'ND', text: 'North Dakota' },
{ value: 'MP', text: 'Northern Mariana Islands' },
{ value: 'OH', text: 'Ohio' },
{ value: 'OK', text: 'Oklahoma' },
{ value: 'OR', text: 'Oregon' },
{ value: 'PW', text: 'Palau' },
{ value: 'PA', text: 'Pennsylvania' },
{ value: 'PR', text: 'Puerto Rico' },
{ value: 'RI', text: 'Rhode Island' },
{ value: 'SC', text: 'South Carolina' },
{ value: 'SD', text: 'South Dakota' },
{ value: 'TN', text: 'Tennessee' },
{ value: 'TX', text: 'Texas' },
{ value: 'UT', text: 'Utah' },
{ value: 'VT', text: 'Vermont' },
{ value: 'VI', text: 'Virgin Islands' },
{ value: 'VA', text: 'Virginia' },
{ value: 'WA', text: 'Washington' },
{ value: 'WV', text: 'West Virginia' },
{ value: 'WI', text: 'Wisconsin' },
{ value: 'WY', text: 'Wyoming' }
]
static accessLevels = [
{ value: 'broker', text: 'Broker' },
{ value: 'employee', text: 'Employee' },
{ value: 'administrator', text: 'Administrator' },
{ value: 'executive', text: 'Executive' }
]
static logoImageSize = { width: 200, height: 50 }
static bigUserImageSize = { width: 300, height: 300 }
static smallUserImageSize = { width: 25, height: 25 }
}

View File

@@ -0,0 +1,2 @@
export { api, NetworkError, ApiError } from './Api'
export { Constants } from './Constants'

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1 @@
<?xml version="1.0" ?><svg enable-background="new 0 0 500 500" id="Layer_1" version="1.1" viewBox="0 0 500 500" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g><g><path d="M427.9,402.1H72.1V128.4h355.9V402.1z M99.4,374.8h301.1v-219H99.4V374.8z"/></g><g><path d="M373.2,292.6H126.8c-30.2,0-54.8-24.6-54.8-54.8v-41.1h27.4v41.1c0,15.1,12.3,27.4,27.4,27.4h246.4 c15.1,0,27.4-12.3,27.4-27.4v-41.1h27.4v41.1C427.9,268.1,403.4,292.6,373.2,292.6z"/></g><g><path d="M304.8,155.8H195.2v-41.1c0-15.1,12.3-27.4,27.4-27.4h54.8c15.1,0,27.4,12.3,27.4,27.4V155.8z M222.6,128.4h54.8v-13.7 h-54.8V128.4z"/></g><g><rect height="41.1" width="27.4" x="318.4" y="210.5"/></g><g><rect height="41.1" width="27.4" x="154.2" y="210.5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 784 B

9
website/src/index.js Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App'
import './index.scss'
ReactDOM.render(
<App />,
document.getElementById('root')
)

14
website/src/index.scss Normal file
View File

@@ -0,0 +1,14 @@
@import '~semantic-ui-css/semantic.min.css';
html, body {
margin: 0;
font-family: sans-serif;
height: 100%;
position: relative;
}
#root {
min-height: 100%;
position: relative;
padding-bottom: 3em;
}

5
website/src/version.js Normal file
View File

@@ -0,0 +1,5 @@
export class VersionInfo {
static version = '0.0.4'
static fullVersion = '0.0.4-20171018.0'
static startYear = '2017'
}