Initial commit
This commit is contained in:
35
website/src/App.js
Normal file
35
website/src/App.js
Normal 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
9
website/src/App.scss
Normal file
@@ -0,0 +1,9 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
html, body {
|
||||
padding-top: 4em;
|
||||
}
|
||||
}
|
||||
70
website/src/Auth/ConfirmEmail.js
Normal file
70
website/src/Auth/ConfirmEmail.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
11
website/src/Auth/ConfirmEmail.scss
Normal file
11
website/src/Auth/ConfirmEmail.scss
Normal 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;
|
||||
}
|
||||
93
website/src/Auth/ForgotPassword.js
Normal file
93
website/src/Auth/ForgotPassword.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
23
website/src/Auth/ForgotPassword.scss
Normal file
23
website/src/Auth/ForgotPassword.scss
Normal 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
125
website/src/Auth/Login.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
3
website/src/Auth/Login.scss
Normal file
3
website/src/Auth/Login.scss
Normal 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; }
|
||||
20
website/src/Auth/Logout.js
Normal file
20
website/src/Auth/Logout.js
Normal 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
|
||||
}
|
||||
}
|
||||
53
website/src/Auth/ProtectedRoute.js
Normal file
53
website/src/Auth/ProtectedRoute.js
Normal 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}`} />
|
||||
}
|
||||
}
|
||||
}
|
||||
93
website/src/Auth/ResetPassword.js
Normal file
93
website/src/Auth/ResetPassword.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
23
website/src/Auth/ResetPassword.scss
Normal file
23
website/src/Auth/ResetPassword.scss
Normal 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;
|
||||
}
|
||||
6
website/src/Auth/index.js
Normal file
6
website/src/Auth/index.js
Normal 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'
|
||||
31
website/src/Dashboard/Dashboard.js
Normal file
31
website/src/Dashboard/Dashboard.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
60
website/src/Dashboard/ProjectCard.js
Normal file
60
website/src/Dashboard/ProjectCard.js
Normal 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>
|
||||
@
|
||||
<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>
|
||||
)
|
||||
}
|
||||
}
|
||||
55
website/src/Dashboard/ProjectCard.scss
Normal file
55
website/src/Dashboard/ProjectCard.scss
Normal 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;
|
||||
}
|
||||
32
website/src/Dashboard/ProjectList.js
Normal file
32
website/src/Dashboard/ProjectList.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
11
website/src/Dashboard/ProjectList.scss
Normal file
11
website/src/Dashboard/ProjectList.scss
Normal 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;
|
||||
}
|
||||
1
website/src/Dashboard/index.js
Normal file
1
website/src/Dashboard/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { Dashboard } from './Dashboard'
|
||||
85
website/src/Dialog/ChangeEmailDialog.js
Normal file
85
website/src/Dialog/ChangeEmailDialog.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
102
website/src/Dialog/ChangePasswordDialog.js
Normal file
102
website/src/Dialog/ChangePasswordDialog.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
33
website/src/Dialog/MessageDialog.js
Normal file
33
website/src/Dialog/MessageDialog.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
26
website/src/Dialog/ProgressDialog.js
Normal file
26
website/src/Dialog/ProgressDialog.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
11
website/src/Dialog/ProgressDialog.scss
Normal file
11
website/src/Dialog/ProgressDialog.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.progress-dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#progress {
|
||||
width: 60%;
|
||||
}
|
||||
18
website/src/Dialog/WaitDialog.js
Normal file
18
website/src/Dialog/WaitDialog.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
37
website/src/Dialog/YesNoMessageDialog.js
Normal file
37
website/src/Dialog/YesNoMessageDialog.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
6
website/src/Dialog/YesNoMessageDialog.scss
Normal file
6
website/src/Dialog/YesNoMessageDialog.scss
Normal file
@@ -0,0 +1,6 @@
|
||||
.yes-no-modal p {
|
||||
font-size: 1.125em;
|
||||
line-height: 1.6em;
|
||||
width: 90%;
|
||||
margin: auto;
|
||||
}
|
||||
6
website/src/Dialog/index.js
Normal file
6
website/src/Dialog/index.js
Normal 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'
|
||||
106
website/src/FilePicker/FilePicker.js
Normal file
106
website/src/FilePicker/FilePicker.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
51
website/src/FilePicker/FilePicker.scss
Normal file
51
website/src/FilePicker/FilePicker.scss
Normal 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;
|
||||
}
|
||||
1
website/src/FilePicker/index.js
Normal file
1
website/src/FilePicker/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { FilePicker } from './FilePicker'
|
||||
9
website/src/Footer/Footer.js
Normal file
9
website/src/Footer/Footer.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import './Footer.scss'
|
||||
import { VersionInfo } from '../version'
|
||||
|
||||
export const Footer = () => (
|
||||
<footer>
|
||||
v{VersionInfo.fullVersion}. © {VersionInfo.startYear} Deighton. All rights reserved.
|
||||
</footer>
|
||||
)
|
||||
14
website/src/Footer/Footer.scss
Normal file
14
website/src/Footer/Footer.scss
Normal 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;
|
||||
}
|
||||
1
website/src/Footer/index.js
Normal file
1
website/src/Footer/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { Footer } from './Footer'
|
||||
9
website/src/Home/Home.js
Normal file
9
website/src/Home/Home.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
|
||||
export class Home extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div />
|
||||
)
|
||||
}
|
||||
}
|
||||
1
website/src/Home/index.js
Normal file
1
website/src/Home/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { Home } from './Home'
|
||||
169
website/src/Navigation/NavBar.js
Normal file
169
website/src/Navigation/NavBar.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
116
website/src/Navigation/NavBar.scss
Normal file
116
website/src/Navigation/NavBar.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
1
website/src/Navigation/index.js
Normal file
1
website/src/Navigation/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { NavBar } from './NavBar'
|
||||
177
website/src/Profile/Profile.js
Normal file
177
website/src/Profile/Profile.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
170
website/src/Profile/ProfileForm.js
Normal file
170
website/src/Profile/ProfileForm.js
Normal 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=' '
|
||||
width={4} onClick={this.props.onChangeEmail} />
|
||||
<Form.Button fluid content={'Change Password'} label=' '
|
||||
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=' ' name='save'
|
||||
validator={this.state.validator} />
|
||||
</Form.Group>
|
||||
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
}
|
||||
12
website/src/Profile/ProfileForm.scss
Normal file
12
website/src/Profile/ProfileForm.scss
Normal 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;
|
||||
}
|
||||
1
website/src/Profile/index.js
Normal file
1
website/src/Profile/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { Profile } from './Profile'
|
||||
230
website/src/Users/UserForm.js
Normal file
230
website/src/Users/UserForm.js
Normal 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=' ' 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=' ' name='remove'
|
||||
validator={this.state.validator} onClick={this.props.onRemove} />
|
||||
<ValidatedButton width={4} size='medium' content='Reset' label=' ' 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=' ' name='submit'
|
||||
validator={this.state.validator} />
|
||||
</Form.Group>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
}
|
||||
8
website/src/Users/UserForm.scss
Normal file
8
website/src/Users/UserForm.scss
Normal file
@@ -0,0 +1,8 @@
|
||||
.user-form {
|
||||
text-align: left;
|
||||
margin: 3em auto 4em auto;
|
||||
}
|
||||
|
||||
.user-form > .fields {
|
||||
margin-bottom: 1.5em !important;
|
||||
}
|
||||
9
website/src/Users/UserFormPlaceholder.js
Normal file
9
website/src/Users/UserFormPlaceholder.js
Normal 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>
|
||||
)
|
||||
7
website/src/Users/UserFormPlaceholder.scss
Normal file
7
website/src/Users/UserFormPlaceholder.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.user-form-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
69
website/src/Users/UserList.js
Normal file
69
website/src/Users/UserList.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
21
website/src/Users/UserList.scss
Normal file
21
website/src/Users/UserList.scss
Normal 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
281
website/src/Users/Users.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
47
website/src/Users/ValidatedEmailIcon.js
Normal file
47
website/src/Users/ValidatedEmailIcon.js
Normal 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> </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> </label>
|
||||
<Button fluid icon='mail outline' color='red' labelPosition='left'
|
||||
content='Resend Email' onClick={this.props.onClick} disabled={this.state.disabled} />
|
||||
</Form.Field>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
3
website/src/Users/ValidatedEmailIcon.scss
Normal file
3
website/src/Users/ValidatedEmailIcon.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.mail-validated-icon {
|
||||
padding-top: 4px;
|
||||
}
|
||||
1
website/src/Users/index.js
Normal file
1
website/src/Users/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { Users } from './Users'
|
||||
59
website/src/Validated/ValidatedActionsButton.js
Normal file
59
website/src/Validated/ValidatedActionsButton.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
61
website/src/Validated/ValidatedButton.js
Normal file
61
website/src/Validated/ValidatedButton.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
48
website/src/Validated/ValidatedCheckbox.js
Normal file
48
website/src/Validated/ValidatedCheckbox.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
52
website/src/Validated/ValidatedContainer.js
Normal file
52
website/src/Validated/ValidatedContainer.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
56
website/src/Validated/ValidatedDatePicker.js
Normal file
56
website/src/Validated/ValidatedDatePicker.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
47
website/src/Validated/ValidatedDropdown.js
Normal file
47
website/src/Validated/ValidatedDropdown.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
63
website/src/Validated/ValidatedInput.js
Normal file
63
website/src/Validated/ValidatedInput.js
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
188
website/src/Validated/Validator.js
Normal file
188
website/src/Validated/Validator.js
Normal 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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
8
website/src/Validated/index.js
Normal file
8
website/src/Validated/index.js
Normal 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'
|
||||
BIN
website/src/assets/images/logo.png
Normal file
BIN
website/src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
426
website/src/helpers/Api.js
Normal file
426
website/src/helpers/Api.js
Normal 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()
|
||||
75
website/src/helpers/Constants.js
Normal file
75
website/src/helpers/Constants.js
Normal 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 }
|
||||
}
|
||||
2
website/src/helpers/index.js
Normal file
2
website/src/helpers/index.js
Normal file
@@ -0,0 +1,2 @@
|
||||
export { api, NetworkError, ApiError } from './Api'
|
||||
export { Constants } from './Constants'
|
||||
BIN
website/src/images/icons/folder-icon.png
Normal file
BIN
website/src/images/icons/folder-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
website/src/images/icons/folder-open-2.png
Normal file
BIN
website/src/images/icons/folder-open-2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
BIN
website/src/images/icons/open-folder.png
Normal file
BIN
website/src/images/icons/open-folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
1
website/src/images/projectPlaceholder.svg
Normal file
1
website/src/images/projectPlaceholder.svg
Normal 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
9
website/src/index.js
Normal 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
14
website/src/index.scss
Normal 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
5
website/src/version.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export class VersionInfo {
|
||||
static version = '0.0.4'
|
||||
static fullVersion = '0.0.4-20171018.0'
|
||||
static startYear = '2017'
|
||||
}
|
||||
Reference in New Issue
Block a user