instantjob-recruiter-client
Version:
a set of tools for creating an instantjob recruiter react client
456 lines (416 loc) • 12.2 kB
JSX
import React, {Component} from 'react'
import classNames from 'classnames/bind'
import {MdDone, MdLock, MdCached, MdInsertDriveFile} from 'react-icons/lib/md'
import Dropzone from 'react-dropzone'
import FileDropzone from './file_dropzone'
import TimeAgo from 'components/utils/time_ago'
import ProgressIndicator from './progress_indicator'
import {instant_job_users_fields} from '../common/constants'
import event_system from '../common/event_system'
import auto_bind from '../common/auto_bind'
import {
property_getter, flatten_array, hash_with_key, property_toggler, empty_set, empty_array, map_hash,
filter_hash, array_from_hash, download_file,
} from '../common/utilities'
import {update_item, toggle_item_id} from '../reducers/base'
const cx = classNames.bind(require('../styles/user_profile.scss'))
const value_width = 200
function field_is_empty({category, values, comment, file, value}) {
switch (category) {
case 'comment':
return !comment
case 'non_exclusive':
return empty_array(values.filter(property_getter('selected')))
case 'file':
return !file
case 'exclusive':
return !value
}
}
const add_empty_property_to_field = (field) => ({
...field,
empty: field_is_empty(field),
})
class UserProfile extends Component {
constructor(props) {
super(props)
this.state = {
...props.user,
progress: 0,
}
this.render_agency = this.render_agency.bind(this)
}
componentDidMount() {
setTimeout(() => this.setState({progress: 6}), 500)
}
render_agency(agency) {
let fields = agency.fields.map(({id}) => this.agency_fields[id]).filter(Boolean)
if (!empty_array(fields)) {
return (
<Agency
{...agency}
fields={fields}
key={agency.id}
/>
)
} else {
return <div key={agency.id}></div>
}
}
render() {
let {agencies} = this.props
let instant_job_fields = instant_job_users_fields.map((field) => ({
...field,
comment: this.state[field.id] || "",
update: (comment) => {
this.setState({[field.id]: comment})
return this.props.update_instant_job_field(field.id, comment)
},
})).map(add_empty_property_to_field)
this.agency_fields = hash_with_key(
flatten_array(this.props.agencies.map(property_getter('fields')))
.map(add_empty_property_to_field)
.filter(({category, values}) => category != 'non_exclusive' || !empty_array(values))
)
let progress_fields = [
...instant_job_fields,
...array_from_hash(this.agency_fields),
].filter(({locked}) => !locked)
return (
<div className={cx('user-profile')}>
<div className={cx('user-profile__header')}>
<div className={cx('user-profile__header-title')}>
Votre profil
</div>
<ProgressIndicator completed={progress_fields.filter(({empty}) => !empty).length} total={progress_fields.length} text={`Complété à ##%`}/>
</div>
<Agency
key={'instantjob'}
title={"Informations InstantJob"}
subtitle={"Ces informations seront partagées avec l'ensemble des employeurs avec qui vous avez choisi de travailler."}
fields={instant_job_fields}
/>
{agencies.map(this.render_agency)}
</div>
)
}
}
UserProfile.defaultProps = {
agencies: [],
user: {},
update_field: () => new Promise((resolve, reject) => resolve()),
update_instant_job_field: () => new Promise((resolve, reject) => resolve()),
}
class Agency extends Component {
constructor(props) {
super(props)
this.render_field = this.render_field.bind(this)
}
render_field(field) {
return (
<Field {...field} key={field.id} />
)
}
render() {
let {fields, title, name, subtitle} = this.props
return (
<div className={cx('user-profile__agency')}>
<div className={cx('user-profile__agency-header')}>
<div className={cx('user-profile__agency-header-title')}>
{name ? `Informations pour ${name}` : title}
</div>
<div className={cx('user-profile__agency-header-subtitle')}>
{subtitle || "Ces informations sont demandées spécifiquement par cet employeur et ne sont pas partagées avec vos autrs employeurs."}
</div>
</div>
{fields.map(this.render_field)}
</div>
)
}
}
export class Field extends Component {
constructor(props) {
super(props)
this.state = {
synchronisations: {},
synchronized: true,
}
this.idGenerator = 1,
this.render_indicator = this.render_indicator.bind(this)
this.render_content = this.render_content.bind(this)
}
render_indicator() {
let state, icon
let {locked, empty} = this.props
let synchronized = empty_set(this.state.synchronisations)
if (locked) {
state = 'locked'
icon = <MdLock />
} else if (!synchronized) {
state = 'synchronizing'
icon = <MdCached />
} else if (empty) {
state = 'empty'
} else {
state = 'synchronized'
icon = <MdDone />
}
return (
<div className={cx('user-profile__field-header-indicator', `user-profile__field-header-indicator_${state}`)}>
{icon}
</div>
)
}
render_content() {
let field = this.props
let props = {
...field,
update: (...input) => {
let synchronisation_id = this.idGenerator++
let toggle_synchronisation = () => this.setState(toggle_item_id(this.state, "synchronisations", synchronisation_id))
toggle_synchronisation()
field.update(...input)
.then(toggle_synchronisation)
}
}
switch (field.category) {
case 'comment':
return (
<CommentField {...props} />
)
case 'non_exclusive':
return (
<NonExclusiveField {...props} />
)
case 'file':
return (
<FileField {...props} />
)
case 'exclusive':
return (
<ExclusiveField {...props} />
)
}
}
render() {
let {name} = this.props
return (
<div className={cx('user-profile__field')}>
<div className={cx('user-profile__field-header')}>
{name}
{this.render_indicator()}
</div>
{this.render_content()}
</div>
)
}
}
class CommentField extends Component {
constructor(props) {
super(props)
this.state = {
comment: props.comment,
focused: false,
}
this.onFocus = this.onFocus.bind(this)
this.onChange = this.onChange.bind(this)
this.onBlur = this.onBlur.bind(this)
}
onFocus() {
this.setState({focused: true})
this.initial_comment = this.state.comment
}
onChange({target: {value: comment}}) {
if (!this.state.focused && comment != this.state.comment) {
this.props.update(comment)
}
this.setState({comment})
}
onBlur() {
this.setState({focused: false})
if (this.state.comment != this.initial_comment) {
this.props.update(this.state.comment)
}
}
componentWillReceiveProps({comment}) {
if (comment != this.state.comment && !this.state.focused) {
this.setState({comment})
}
}
render() {
let {comment, focused} = this.state
let className = cx('user-profile__field-input', {'user-profile__field-input_empty': !comment, 'user-profile__field-input_focused': focused})
if (this.props.locked) {
return (
<div className={className}>
{comment}
</div>
)
} else {
return (
<input
className={className}
type="text"
value={comment || ""}
onFocus={this.onFocus}
onChange={this.onChange}
onBlur={this.onBlur}
/>
)
}
}
}
CommentField.defaultProps = {
comment: "",
}
class NonExclusiveField extends Component {
constructor(props) {
super(props)
this.state = {
width: 500,
}
this.update_width = this.update_width.bind(this)
this.render_value = this.render_value.bind(this)
this.toggle_value = this.toggle_value.bind(this)
}
componentDidMount() {
this.update_width()
this.cancel_update_width = event_system.register("window_resize", this.update_width)
}
componentWillUnmount() {
this.cancel_update_width()
}
update_width() {
let width = $(this.values_container).width()
if (width != this.state.width) {
this.setState({width})
}
}
toggle_value(index) {
return () => {
let {values, update} = this.props
update([
...values.slice(0, index),
property_toggler('selected')(values[index]),
...values.slice(index + 1),
])
}
}
render_value(width) {
return ({id, name, selected}, index) => (
<div
onClick={this.toggle_value(index)}
key={id}
className={cx('user-profile__field-values-value', {'user-profile__field-values-value_selected': selected})}
style={{width}}
>
<div className={cx('user-profile__field-values-value-outline')}>
<div className={cx('user-profile__field-values-value-name')}>
{name}
</div>
<div className={cx('user-profile__field-values-value-icon')}>
{selected ? (
<MdDone />
) : null}
</div>
</div>
</div>
)
}
render() {
let available_width = this.state.width
let columns_number = Math.floor(available_width / value_width) || 1
let width = `${100 / columns_number}%`
let {values} = this.props
return (
<div
ref={(values_container) => this.values_container = values_container}
className={cx('user-profile__field-values')}
>
{values.map(this.render_value(width))}
</div>
)
}
}
class FileField extends Component {
constructor(props) {
super(props)
auto_bind(this)
}
on_files_drop([file]) {
this.props.update(file)
}
view() {
let {file: {url, name}} = this.props
download_file(url, name)
}
render() {
let {file} = this.props
if (file) {
if (file.uploading) {
return (
<ProgressIndicator small className={cx('user-profile__field-dropzone')} completed={file.uploading} total={100} text="##%"/>
)
} else {
let {updated_at, url} = file
return (
<div className={cx('user-profile__field-file')}>
<div onClick={this.view} className={cx('user-profile__field-file-preview')}>
<MdInsertDriveFile />
</div>
<div className={cx('user-profile__field-file-updated-at')}>
Mis à jour <TimeAgo>{updated_at}</TimeAgo>
</div>
<Dropzone style={{}} onDrop={this.on_files_drop}>
<div className={cx('user-profile__field-file-update')}>
Choisir un autre fichier
</div>
</Dropzone>
</div>
)
}
} else {
return (
<div className={cx('user-profile__field-dropzone')}>
<FileDropzone on_files_drop={this.on_files_drop}>
Déposer un fichier
</FileDropzone>
</div>
)
}
}
}
class ExclusiveField extends Component {
constructor(props) {
super(props)
auto_bind(this)
}
update(values) {
const selected_value_id = this.get_selected_value_id()
values.forEach((value) => {
if (value.id == selected_value_id && !value.selected) {
return this.props.update(null)
}
if (value.selected && value.id != selected_value_id) {
return this.props.update(value)
}
})
return new Promise((resolve, reject) => resolve())
}
get_selected_value_id() {
const {value} = this.props
const {id} = value || {}
return id
}
render() {
const {values} = this.props
const selected_value_id = this.get_selected_value_id()
return (
<NonExclusiveField
values={values.map((value) => ({...value, selected: value.id == selected_value_id}))}
update={this.update}
/>
)
}
}
export default UserProfile