kitten-components
Version:
Front-end components library
478 lines (396 loc) • 12.7 kB
JavaScript
// Simulator that lets users select an amount and an installment, to start
// simulating a loan.
import React from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'
import { numberUtils } from 'kitten/helpers/utils/number'
import { SliderWithTooltipAndPower } from 'kitten/components/sliders/slider-with-tooltip-and-power'
import { TextInputWithUnit } from 'kitten/components/form/text-input-with-unit'
export class LoanSimulator extends React.Component {
constructor(props) {
super(props)
this.state = {
amount: props.initialAmount * 1,
installmentAmount: props.initialInstallment,
dragged: !!props.initialInstallment,
touched: props.initialTouched,
}
this.handleFocus = this.handleFocus.bind(this)
this.handleAmountChange = this.handleAmountChange.bind(this)
this.handleEnter = this.handleEnter.bind(this)
this.handleInstallmentLabelClick = this.handleInstallmentLabelClick.bind(
this,
)
this.handleInstallmentChange = this.handleInstallmentChange.bind(this)
this.handleInstallmentAction = this.handleInstallmentAction.bind(this)
}
handleFocus(e) {
this.setState({
touched: false,
installmentAmount: null,
})
}
handleAmountChange(e) {
this.setState({ amount: e.target.value })
}
handleEnter(e) {
this.setState({ touched: true })
}
handleInstallmentLabelClick() {
this.refs.content.focusSlider()
}
// on slider click or on grab change
handleInstallmentChange(value) {
this.setState({
installmentAmount: value,
dragged: true,
})
}
handleInstallmentAction() {
this.setState({ touched: true })
}
duration() {
if (this.state.installmentAmount)
return Math.ceil(this.state.amount / this.state.installmentAmount)
}
commissionRate() {
const duration = this.duration()
for (let i = 0, len = this.props.commissionRules.length; i < len; i++) {
let rule = this.props.commissionRules[i]
if (!rule.durationMax || duration <= rule.durationMax) return rule.rate
}
}
commissionAmount() {
return this.commissionRate() * this.state.amount
}
installmentMin() {
const installmentStep = this.installmentStep()
const value = this.state.amount / this.props.durationMax
const min = Math.ceil(value / installmentStep) * installmentStep
if (min > this.state.amount * 1) return this.state.amount
else return min
}
installmentMax() {
return this.state.amount * 1
}
installmentStep() {
if (this.state.installmentAmount > 1000) return 100
if (this.state.installmentAmount > 200) return 10
return 1
}
error() {
if (!this.state.touched) return null
return this.amountError()
}
amountError() {
if (!this.state.amount) return this.props.amountEmptyError
if (
!numberUtils.isNumber(this.state.amount) ||
this.state.amount < this.props.amountMin ||
this.state.amount > this.props.amountMax
)
return this.props.amountOutOfBoundsError
}
render() {
return (
<LoanSimulatorContent
ref="content"
{...this.props}
{...this.state}
onFocus={this.handleFocus}
onAmountChange={this.handleAmountChange}
onEnter={this.handleEnter}
onInstallmentLabelClick={this.handleInstallmentLabelClick}
onInstallmentChange={this.handleInstallmentChange}
onInstallmentAction={this.handleInstallmentAction}
duration={this.duration()}
commissionAmount={this.commissionAmount()}
commissionRate={this.commissionRate()}
installmentMin={this.installmentMin()}
installmentMax={this.installmentMax()}
installmentStep={this.installmentStep()}
error={this.error()}
amountError={this.amountError()}
/>
)
}
}
class LoanSimulatorContent extends React.Component {
constructor(props) {
super(props)
this.handleAmountKeyDown = this.handleAmountKeyDown.bind(this)
this.handleInstallmentChange = this.handleInstallmentChange.bind(this)
}
// Allow parents to focus the slider
focusSlider() {
this.slider.focus()
}
handleAmountKeyDown(e) {
// when pressing enter
if (e.keyCode == 13) {
this.focusSlider()
this.props.onEnter()
}
}
handleInstallmentChange(value, ratio) {
this.amount.blur()
this.props.onInstallmentChange(value, ratio)
}
toCurrency(cents) {
if (isNaN(cents)) return null
return (cents / 100).toLocaleString(this.props.locale)
}
sliderIsActive() {
return (
!this.props.amountError &&
this.props.dragged &&
this.props.installmentAmount
)
}
renderCommission() {
if (!this.props.displayCommission) return
const active = this.sliderIsActive()
const amount = active
? this.toCurrency(this.props.commissionAmount * 100)
: '--'
let commissionAmount = ' 0 '
let exemptionText = null
if (!this.props.feesExemption) {
commissionAmount = ` ${amount} `
} else {
const text = `
(${this.props.feesExemptionLabel}
${amount}
${this.props.currencySymbol})
`
exemptionText = (
<span className="k-LoanSimulator__feesExemption">{text}</span>
)
}
commissionAmount += ` ${this.props.currencySymbol}`
return (
<div className="k-LoanSimulator__commission">
{this.props.commissionLabel}
<span
className={classNames({
'k-u-text--active': active,
'k-u-text--inactive': !active,
})}
>
{commissionAmount}
</span>
{exemptionText}
</div>
)
}
renderDurationError() {
const { duration, touched, requiredDurationError } = this.props
if (!touched || duration > 0 || !requiredDurationError) return
return (
<span className="k-LoanSimulator__durationError">
{this.props.requiredDurationError}
</span>
)
}
renderButton() {
if (!this.props.actionLabel) return
return (
<div className="k-LoanSimulator__actions">
<button className="k-Button k-Button--helium k-Button--big">
{this.props.actionLabel}
</button>
</div>
)
}
render() {
const { label, dragged, touched, duration, error } = this.props
const amountValid = !this.props.amountError
const showResult = !error && touched && duration
const installmentMin = amountValid ? this.props.installmentMin : 0
const installmentMax = amountValid ? this.props.installmentMax : 0
const installmentAmount = amountValid ? this.props.installmentAmount : 0
const installmentPercentage = amountValid
? this.props.installmentPercentage
: 0
let errorClass, errorTag, tooltipClass, tooltipText
if (error) {
errorClass = 'is-error'
errorTag = <p className="k-LoanSimulator__amount__error">{error}</p>
}
if (this.sliderIsActive()) {
const durationSymbol =
duration === 1
? this.props.durationSymbol
: this.props.durationSymbolPlural
const installmentText = `
${this.toCurrency(installmentAmount * 100)}
${this.props.installmentSymbol}
`
const durationText = `
${this.props.durationText}
${duration}
${durationSymbol}
`
tooltipClass = null
tooltipText = [
<div key="1" className="k-LoanSimulator__installment">
{installmentText}
</div>,
<div key="2" className="k-LoanSimulator__duration">
{durationText}
</div>,
]
} else {
tooltipClass = 'is-inactive'
tooltipText = this.props.sliderPlaceholder
}
let durationInput
if (this.props.durationName)
durationInput = (
<input
type="hidden"
name={this.props.durationName}
value={this.props.duration || ''}
/>
)
return (
<div className={classNames('k-LoanSimulator', errorClass)}>
<div className="k-LoanSimulator__amount">
<label
className="k-Label k-LoanSimulator__label"
htmlFor="loan-simulator-amount"
>
{this.props.amountLabel}
</label>
<TextInputWithUnit
ref={input => (this.amount = input)}
error={error}
id="loan-simulator-amount"
name={this.props.amountName}
type="number"
min={this.props.amountMin}
max={this.props.amountMax}
defaultValue={this.props.initialAmount}
onFocus={this.props.onFocus}
onChange={this.props.onAmountChange}
onKeyDown={this.props.onAmountKeyDown}
placeholder={this.props.amountPlaceholder}
unit={this.props.currencySymbol}
/>
{errorTag}
</div>
<div>
<label
className="k-Label k-LoanSimulator__label"
onClick={this.props.onInstallmentLabelClick}
>
{this.props.installmentLabel}
</label>
<SliderWithTooltipAndPower
ref={input => (this.slider = input)}
step={this.props.installmentStep}
min={installmentMin}
max={installmentMax}
power={2}
name={this.props.installmentName}
value={installmentAmount}
onChange={this.handleInstallmentChange}
onAction={this.props.onInstallmentAction}
tooltipClass={tooltipClass}
tooltipText={tooltipText}
/>
{this.renderCommission()}
{durationInput}
{this.renderDurationError()}
</div>
{this.renderButton()}
</div>
)
}
}
LoanSimulator.propTypes = {
// Label for amount input
amountLabel: PropTypes.string,
// Name attribute for the amount input (if needed)
amountName: PropTypes.string,
// Name attribute for the hidden installment input (if needed)
installmentName: PropTypes.string,
// Name attribute for the hidden duration input (if needed)
durationName: PropTypes.string,
// Placeholder for amount input
amountPlaceholder: PropTypes.string,
// Bounds for accepted amount
amountMin: PropTypes.number,
amountMax: PropTypes.number,
// Default amount
initialAmount: PropTypes.number,
// Set this to true to show errors on first use
initialTouched: PropTypes.bool,
// Error text when the amount is empty or non-numerical
amountEmptyError: PropTypes.string,
// Error text when the amount is over or under the min and max
amountOutOfBoundsError: PropTypes.string,
// Error text when the duration has not been set
requiredDurationError: PropTypes.string,
// Display commission if requested
displayCommission: PropTypes.bool,
commissionLabel: PropTypes.string,
commissionRules: PropTypes.array,
// Label before the slider
installmentLabel: PropTypes.string,
// Text before the computed duration
durationText: PropTypes.string,
// Bounds for the computed duration
durationMin: PropTypes.number,
durationMax: PropTypes.number,
// Duration value (months)
durationSymbol: PropTypes.string,
durationSymbolPlural: PropTypes.string,
// Currency
currencySymbol: PropTypes.string,
// Installment
installmentSymbol: PropTypes.string,
// Locale to format amounts correctly
locale: PropTypes.string,
// Submit button
actionLabel: PropTypes.string,
}
LoanSimulator.defaultProps = {
amountLabel: 'I need',
amountPlaceholder: '',
amountMin: 1,
amountMax: 10000,
initialAmount: null,
initialTouched: false,
amountEmptyError: 'Amount cannot be empty',
amountOutOfBoundsError: 'Amount is either too big or too small',
requiredDurationError: 'Duration is required',
displayCommission: false,
feesExemption: false,
feesExemptionLabel: 'instead of',
commissionLabel: 'Fees:',
// The `commissionRules` prop has to be an array containing a `durationMax`
// rule. This will return the first `rate` which matches the rule for the
// current `duration`.
//
// Example `commissionRules` prop:
// [
// { durationMax: 12, rate: 0.3 },
// { durationMax: 20, rate: 0.2 },
// { rate: 0.1 }
// ]
commissionRules: [],
installmentLabel: "I'd like to reimburse",
initialInstallment: null,
durationText: 'during',
durationMin: 1,
durationMax: 36,
durationSymbol: 'month',
durationSymbolPlural: 'months',
currencySymbol: '$',
installmentSymbol: '$/month',
locale: 'en',
actionLabel: null,
}
// DEPRECATED: do not use default export.
export default LoanSimulator