Merge pull request #105 from sgmap/angle-mort-des-resultats

Redesign de l'interface de simulation, dont la barre de résultats
pull/116/head
Mael 2017-10-23 18:00:34 +02:00 committed by GitHub
commit 4162afb1fe
16 changed files with 186 additions and 322 deletions

View File

@ -22,6 +22,7 @@
"npm": "^5.3.0",
"ramda": "0.24.1",
"react": "^16.0.0",
"react-addons-css-transition-group": "^15.6.2",
"react-dom": "^16.0.0",
"react-helmet": "^5.2.0",
"react-redux": "^5.0.6",
@ -30,6 +31,7 @@
"react-scroll": "^1.5.4",
"react-select": "^1.0.0-rc.10",
"react-select-fast-filter-options": "^0.2.3",
"react-transition-group": "^2.2.1",
"react-virtualized": "^9.10.1",
"react-virtualized-select": "^3.1.0",
"reduce-reducers": "^0.1.2",

View File

@ -94,8 +94,6 @@
simulateur:
titre: Simulateur CDD
sous-titre: Découvrir le surcoût employeur du CDD par rapport au CDI
résultats: Le coût du travail faisant ressortir les cotisations spécifiques au CDD.
indice: Par mois
introduction:
notes:
- icône: fa-handshake-o
@ -104,7 +102,6 @@
- icône: fa-balance-scale
texte: Votre contrat ne peut donc avoir ni pour objet ni pour effet de pourvoir durablement un emploi lié à l'activité normale et permanente de l'entreprise.
titre: Votre obligation
motivation: Découvrez en quelques clics le montant des obligations liées au CDD
# CIF, majoration chômage, indemnité de fin de contrat, indemnité compensatrice des congés payés
hypothèses:
contrat salarié . type de contrat: CDD

View File

@ -194,8 +194,6 @@
titre: Simulateur de coût d'embauche
sous-titre: Découvrir le coût d'embauche ou le salaire réel
résultats: Le salaire net à partir du brut ou vice-versa, et les cotisations
introduction:
motivation: Découvrez le vrai coût du travail
- espace: contrat salarié

View File

@ -34,7 +34,7 @@ export default class HomeEmbauche extends Component {
</div>
<div>
<span>Nouveau</span>
<a href="/simu/surcoût-CDD/intro">Simuler le surcoût CDD (beta) <i className="fa fa-hand-o-right" aria-hidden="true"></i></a>
<a href="/simu/surcoût-CDD">Simuler le surcoût CDD (beta) <i className="fa fa-hand-o-right" aria-hidden="true"></i></a>
</div>
</div>
</div>

View File

@ -1,51 +1,38 @@
#results {
padding: .1em;
background: #333350;
font-size: 80%;
color: white;
padding: .6em 0;
text-align: center;
width: 100%;
height: 12em;
position: fixed;
bottom: 0;
left: 0;
box-shadow: 1px -7px 20px 2px #ccc;
left: 50%;
bottom: 2.5%;
width: 90%;
max-width: 45em;
margin: 0 auto;
padding: 0;
background: #2975D1;
color: white;
font-size: 120%;
box-shadow: 0px 0px 20px 2px rgba(0, 0, 0, 0.25);
opacity: 0;
transform: translateY(12em);
transform: translate(-50%, 12em);
transition: transform .5s;
transition-delay: .3s;
transition-timing-function: cubic-bezier(0, 1.01, 0.24, 1)
}
#results.show {
transform: translateY(0);
transform: translate(-50%, 0);
opacity: 1;
}
#results-actions,
#results-titles {
display: inline-block;
float: left;
width: 18%;
margin: 0;
padding: 0 0 0 2em;
}
#results-actions {
display: flex;
align-items: center;
justify-content: space-around;
height: 100%;
background: #333350;
}
#toSimulation {
font-size: 190%;
font-size: 150%;
color: white;
background: #4A89DC;
padding: .6em .6em;
margin-left: .6em;
line-height: 1.8em;
text-decoration: none;
border-radius: .2em;
position: relative;
@ -54,82 +41,46 @@
margin-right: .6em;
}
#results-titles {
color: white;
line-height: 1.2em;
font-weight: 400;
font-size: 120%;
text-align: left;
}
#results-titles h2 {
font-size: 250%;
margin: .4em 0;
}
#results-titles p {
color: inherit;
}
#results-titles i {
margin: 0 .3em;
}
#resultText {
#results h2 {
margin: .6em;
font-weight: 600;
font-size: 100%;
text-align: left;
font-size: 125%;
}
#results h2 i {
margin-right: .6em;
}
#results h2 small {
opacity: 0.7;
font-size: calc(50% + .2vw);
font-weight: 400;
}
#results h2 span {
font-size: 70%;
margin: 0 .6em
}
#results ul {
display: inline-flex;
justify-content: space-around;
height: 100%;
width: 80%;
list-style: none;
padding-left: 0;
margin: 0;
display: flex;
align-items: center;
flex-wrap: wrap;
height: 70%;
margin: .6em auto 1.2em;
}
#results li {
margin: 0 1em 0 2em;
display: inline-block;
width: 100%;
}
@media (max-width: 1280px) {
#results .rule-type {
display: none;
}
#results {
padding: 0;
}
#results-titles {
width: 100%;
text-align: center;
margin-bottom: .6em;
}
#results-titles p {
margin: 0;
margin-right: 3em;
}
#results-titles h2 {
font-size: 150%;
display: none;
}
#results-titles #resultText {
font-size: 120%
}
#results h2 {
margin: 0.3em 1em 0 0;
display: inline-block;
}
#results-titles > p {
display: inline-block;
}
#results-titles #understandTip {
display: none;
}
#results ul {
width: 100%;
font-size: 90%;
}
}

View File

@ -34,9 +34,7 @@ export default class Results extends Component {
if (!explanation) return null
let onRulePage = R.contains('/regle/')(location.pathname),
hint = analysedSituation.root.simulateur && analysedSituation.root.simulateur.indice
let onRulePage = R.contains('/regle/')(location.pathname)
return (
<section id="results" className={classNames({show: showResults})}>
{onRulePage && conversationStarted ?
@ -46,15 +44,13 @@ export default class Results extends Component {
</Link>
</div>
: <div id="results-titles">
<h2>{hint || "Vos résultats"}: <i className="fa fa-hand-o-right" aria-hidden="true"></i></h2>
{do {let text = R.path(['simulateur', 'résultats'])(analysedSituation.root)
text &&
<p id="resultText">{text}</p>
}}
<h2><i className="fa fa-calculator" aria-hidden="true"></i>{explanation.length == 1 ? 'Votre résultat' : 'Vos résultats'}<span>·</span><small>Cliquez pour comprendre chaque calcul</small></h2>
</div>
}
<ul>
{explanation.map( rule => <RuleValueVignette key={rule.nom} {...rule} conversationStarted={conversationStarted} />)}
{explanation.map( rule => <li key={rule.nom}>
<RuleValueVignette {...rule} conversationStarted={conversationStarted} />
</li>)}
</ul>
</section>
)

View File

@ -1,5 +1,5 @@
#sim {
padding: 3em 0; /* For the warning message */
margin: 1% auto;
/*background-image: radial-gradient(ellipse at center, white -160%, rgba(255,255,255,0) 100%);*/
/*background-image: radial-gradient(ellipse at center, #4A89DC -160%,#333350 70%);*/
color: #333350;
@ -7,6 +7,9 @@
height: 100%;
padding-bottom: 10%;
padding: 1em;
max-width: 50em;
}
#sim p {
@ -15,8 +18,7 @@
#sim > h1 {
color: inherit;
margin-top: 0;
text-align: center;
margin: 0;
font-size: 350%;
font-weight: 800;
}
@ -24,23 +26,14 @@
#simSubtitle {
margin-top: -.5em;
text-align: center;
font-size: 110%;
font-size: 120%;
font-weight: 300;
margin-bottom: 2.5em;
}
#sim .centered {
/*width: 40%;*/
width: 50em;
max-width: 90%;
margin: 0 auto;
}
#sim .intro {
font-style: italic;
font-size: 100%;
margin-bottom: 3%;
font-size: 110%;
}
#sim .intro > div {
margin: 1em 0;
@ -57,13 +50,6 @@
width: 80%;
}
#sim .remarks p {
opacity: .9;
padding-left: 1em;
border-left: 10px solid rgba(255, 255, 255, 0.2);
font-style: italic;
}
#sim .action {
margin-top: 5%;
margin-bottom: 3%;
@ -72,31 +58,30 @@
text-align: center;
}
#sim .action button {
#sim .intro button {
color: white;
display: block;
text-align: center;
background: #4A89DC;
padding: .6em 1.2em;
padding: .3em 1em;
font-size: 140%;
margin: 1em auto;
width: 12em;
margin: 2em auto;
width: 8em;
border: none;
box-shadow: 0px 9px 14px 0px rgba(0, 0, 0, 0.1)
box-shadow: 0px 9px 14px 0px rgba(0, 0, 0, 0.1);
opacity: 0.95
}
#sim .action button:hover {
#sim .intro button:hover {
box-shadow: none;
opacity: .95;
opacity: 1;
}
#conversation {
margin: 3em auto;
padding: 0 1em;
margin-top: 6%;
font-size: 110%;
line-height: normal;
min-height: 10em;
max-width: 50em;
}
@ -108,14 +93,13 @@
#foldedSteps .header {
margin-bottom: 1em;
text-align: center;
}
#foldedSteps .header h3 {
display: inline;
}
#foldedSteps .header button {
font-size: 80%;
color: #4A89DC;
color: #2975D1;
border: none;
}
#foldedSteps .header button i {
@ -126,21 +110,6 @@
#fin {
margin: 0 auto;
width: 80%;
display: flex;
align-items: flex-end;
width: 30em;
font-style: italic;
}
#fin-text {
width: 50%;
margin-left: 2em;
display: inline-block;
}
#fin p:first-of-type {
font-weight: bold
}
#fin img {
width: 25%;
display: inline-block;
}

View File

@ -14,7 +14,7 @@ import './Simulateur.css'
import {capitalise0} from '../utils'
import Conversation from './conversation/Conversation'
import ReactPiwik from './Tracker';
import ReactPiwik from './Tracker'
let situationSelector = formValueSelector('conversation')
@ -34,15 +34,18 @@ let situationSelector = formValueSelector('conversation')
resetForm: () => dispatch(reset('conversation'))
})
)
export default class extends React.Component {
export default class extends Component {
state = {
started: false
}
componentWillMount() {
let {
match: {
params: {
name: encodedName
match: {
params: {
name: encodedName
}
}
}
} = this.props,
} = this.props,
name = decodeRuleName(encodedName),
existingConversation = this.props.foldedSteps.length > 0
@ -58,12 +61,12 @@ export default class extends React.Component {
if (!this.rule.formule) return <Redirect to={"/regle/" + this.name}/>
let
started = !this.props.match.params.intro,
{foldedSteps, extraSteps, unfoldedSteps, situation, situationGate} = this.props,
{started} = this.state,
{foldedSteps, extraSteps, unfoldedSteps, situation, situationGate, themeColours} = this.props,
sim = path =>
R.path(R.unless(R.is(Array), R.of)(path))(this.rule.simulateur || {}),
reinitalise = () => {
ReactPiwik.push(['trackEvent', 'restart', '']);
ReactPiwik.push(['trackEvent', 'restart', ''])
this.props.resetForm(this.name)
this.props.startConversation(this.name)
},
@ -80,8 +83,8 @@ export default class extends React.Component {
{sim('sous-titre') &&
<div id="simSubtitle">{sim('sous-titre')}</div>
}
{sim(['introduction', 'notes']) &&
<div className="intro centered">
{!started && sim(['introduction', 'notes']) &&
<div className="intro">
{sim(['introduction', 'notes']).map( ({icône, texte, titre}) =>
<div key={titre}>
<i title={titre} className={"fa "+icône} aria-hidden="true"></i>
@ -90,27 +93,13 @@ export default class extends React.Component {
</span>
</div>
)}
<button onClick={() => this.setState({started: true})}>J'ai compris</button>
</div>
}
{
// Tant que le bouton 'C'est parti' n'est pas cliqué, on affiche l'intro
!started ?
<div>
<div className="action centered">
{createMarkdownDiv(sim(['introduction', 'motivation'])) || <p>Simulez cette règle en quelques clics</p>}
<button onClick={() => this.props.history.push(`/simu/${this.encodedName}`) }>
C'est parti !
</button>
</div>
<div className="remarks centered">
<p>
N'hésitez pas à nous écrire <Link to="/contact">
<i className="fa fa-envelope-open-o" aria-hidden="true" style={{margin: '0 .3em'}}></i>
</Link> ! La loi française est très ciblée, et donc complexe. Nous pouvons la rendre plus transparente.
</p>
</div>
</div>
: <Conversation initialValues={ R.pathOr({},['simulateur','par défaut'], sim) } {...{foldedSteps, unfoldedSteps, extraSteps, reinitalise, situation, situationGate}}/>}
{ (started || !sim(['introduction', 'notes'])) &&
<Conversation initialValues={ R.pathOr({},['simulateur','par défaut'], sim) }
{...{foldedSteps, unfoldedSteps, extraSteps, reinitalise, situation, situationGate, textColourOnWhite: themeColours.textColourOnWhite}}/>
}
</div>
)

View File

@ -11,7 +11,7 @@ import Scroll from 'react-scroll'
})
export default class Conversation extends Component {
render() {
let {foldedSteps, unfoldedSteps, extraSteps, reinitalise, situation, situationGate} = this.props
let {foldedSteps, unfoldedSteps, extraSteps, reinitalise, situation, situationGate, textColourOnWhite} = this.props
Scroll.animateScroll.scrollToBottom()
return (
@ -21,7 +21,7 @@ export default class Conversation extends Component {
<div id="foldedSteps">
<div className="header" >
<h3>Vos réponses</h3>
<button onClick={reinitalise}>
<button onClick={reinitalise} style={{color: textColourOnWhite}}>
<i className="fa fa-trash" aria-hidden="true"></i>
Tout effacer
</button>
@ -37,6 +37,8 @@ export default class Conversation extends Component {
))}
</div>
}
{unfoldedSteps.length == 0 &&
<Conclusion affiner={!R.isEmpty(extraSteps)}/>}
{ !R.isEmpty(extraSteps) &&
<div id="foldedSteps">
<div className="header" >
@ -64,8 +66,9 @@ export default class Conversation extends Component {
/>
}}
</div>
{unfoldedSteps.length == 0 &&
<Conclusion simu={this.name}/>}
{R.isEmpty(unfoldedSteps) &&
<Satisfaction simu={this.props.simu}/>
}
</div>
<Aide />
</div>
@ -73,22 +76,11 @@ export default class Conversation extends Component {
}
}
class Conclusion extends Component {
render() {
return (
<div id="fin">
<img src={require('../../images/fin.png')} />
<div id="fin-text">
<p>
Votre simulation est terminée !
</p>
<p>
N'hésitez pas à modifier vos réponses, ou cliquez sur vos résultats pour comprendre le calcul.
</p>
<Satisfaction simu={this.props.simu}/>
</div>
</div>
)
}
}
let Conclusion = ({ affiner }) => (
<div id="fin">
<p>
Vous pouvez maintenant modifier vos réponses{" "}
{affiner && "ou affiner votre situation"} : vos résultats ci-dessous seront mis à jour.
</p>
</div>
)

View File

@ -53,13 +53,13 @@ export default class Input extends Component {
</button>
</span>
{this.renderSuggestions()}
{this.renderSuggestions(themeColours)}
{inputError && <span className="step-input-error">{error}</span>}
</span>
)
}
renderSuggestions(){
renderSuggestions(themeColours){
let {setFormValue, submit, suggestions} = this.props.stepProps
if (!suggestions) return null
return (
@ -69,7 +69,8 @@ export default class Input extends Component {
<li key={value}
onClick={e => setFormValue('' + value) && submit() && e.preventDefault()}
onMouseOver={() => setFormValue('' + value) && this.setState({suggestedInput: true})}
onMouseOut={() => setFormValue('') && this.setState({suggestedInput: false})}>
onMouseOut={() => setFormValue('') && this.setState({suggestedInput: false})}
style={{color: themeColours.colour}}>
<a href="#" title="cliquer pour valider">{text}</a>
</li>
)}

View File

@ -147,7 +147,7 @@
}
#foldedSteps {
margin: 2em 0 4em;
margin: 3em 0;
}
.step.question .variant {
@ -305,13 +305,10 @@ fieldset > .ignore {
margin-left: .6em;
}
.step .inputSuggestions a {
color: #4A89DC;
padding: .1em .6em;
font-weight: 600;
}
.step .inputSuggestions a:hover {
background: #4A89DC;
color: white;
text-decoration: none;
}

View File

@ -1,9 +1,4 @@
.RuleValueVignette {
margin: 0 1em 0;
text-align: center;
width: 25%;
}
.RuleValueVignette li a {
text-decoration: none;
@ -11,84 +6,43 @@
.RuleValueVignette .rule-type {
color: white;
border: none;
font-size: 85%;
line-height: 2em;
font-weight: 600;
margin: .6em 0 .1em;
}
.RuleValueVignette .rule-box {
padding: .6em 1em;
color: #333350;
background: white;
border-radius: 3px;
white-space: nowrap;
height: 8em;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
justify-content: space-between;
}
.RuleValueVignette .rule-box:hover {
background: rgba(255, 255, 255, 0.16)
}
.RuleValueVignette .rule-name {
font-size: 175%;
font-weight: 600;
.RuleValueVignette .rule-box > span {
display: inline-block;
}
.RuleValueVignette p {
margin: 0;
padding: 0 0;
font-size: 120%;
color: inherit;
width: 100%;
.RuleValueVignette .rule-value {
transition: background .8s;
}
.RuleValueVignette.number p {
color: #4A89DC;
font-weight: bold;
}
.RuleValueVignette.unsatisfied p {
.RuleValueVignette .rule-value .unsatisfied {
font-style: italic;
}
.RuleValueVignette.irrelevant p {
font-weight: 600;
.RuleValueVignette .rule-value .irrelevant {
font-style: normal;
}
.RuleValueVignette p .figure {
font-size: 250%;
}
.RuleValueVignette:not(.unsatisfied):not(.irrelevant) .rule-box {
border-bottom: .8em solid #4A89DC;
}
.RuleValueVignette:hover .rule-box {
background: #ddd;
}
.RuleValueVignette.irrelevant .rule-box {
background: rgba(255, 255, 255, 0.35);
.RuleValueVignette .rule-value .figure {
font-weight: bold;
}
.RuleValueVignette.irrelevant .rule-type {
color: rgba(255, 255, 255, 0.35);
/* Animation of summary figures changes : flash ! */
.flash-enter {
background: rgba(255, 255, 255, 1);
}
.RuleValueVignette.irrelevant .rule-name {
text-decoration: line-through;
}
@media (max-width: 1280px) {
.RuleValueVignette .rule-box p {
padding: 0.6em;
}
.RuleValueVignette .rule-name {
font-size: 150%;
}
.flash-leave {
/* Completely hide the button while it's being animated and before it's removed from the DOM. */
display: none;
}

View File

@ -1,11 +1,13 @@
import React from "react"
import {Link} from 'react-router-dom'
import {encodeRuleName} from 'Engine/rules'
import classNames from 'classnames'
import {capitalise0} from '../../utils'
let fmt = new Intl.NumberFormat('fr-FR').format
export let humanFigure = decimalDigits => value => fmt(value.toFixed(decimalDigits))
import './RuleValueVignette.css'
import { Link } from "react-router-dom"
import { encodeRuleName } from "Engine/rules"
import classNames from "classnames"
import { capitalise0 } from "../../utils"
let fmt = new Intl.NumberFormat("fr-FR").format
export let humanFigure = decimalDigits => value =>
fmt(value.toFixed(decimalDigits))
import "./RuleValueVignette.css"
import ReactCSSTransitionGroup from "react-addons-css-transition-group"
export default ({
name,
@ -15,36 +17,49 @@ export default ({
nodeValue: ruleValue
}) =>
do {
let
unsatisfied = ruleValue == null,
let unsatisfied = ruleValue == null,
irrelevant = ruleValue == 0,
number = typeof ruleValue == 'number' && ruleValue > 0
number = typeof ruleValue == "number" && ruleValue > 0
;<span
key={name}
className={classNames('RuleValueVignette', { unsatisfied, irrelevant, number })}
className={classNames("RuleValueVignette", {
unsatisfied,
irrelevant,
number
})}
>
<Link to={"/regle/" + encodeRuleName(name)}>
<div className="rule-type">
{type}
</div>
<div className="rule-type">{type}</div>
<div className="rule-box">
<div className="rule-name">
{titre || capitalise0(name)}
</div>
<p>
{conversationStarted &&
(irrelevant
? "Vous n'êtes pas concerné"
: unsatisfied
? "En attente de vos réponses..."
: <div><span className="figure">
{humanFigure(2)(ruleValue) + "€"}
</span>
<p><i className="fa fa-lightbulb-o" aria-hidden="true"></i><em>Pourquoi ?</em></p>
</div>)}
</p>
<span className="rule-name">{titre || capitalise0(name)}</span>
<RuleValue
{...{ unsatisfied, irrelevant, conversationStarted, ruleValue }}
/>
</div>
</Link>
</span>
}
let RuleValue = ({ unsatisfied, irrelevant, conversationStarted, ruleValue }) =>
do {
let [className, text] = irrelevant
? ["irrelevant", "Vous n'êtes pas concerné"]
: unsatisfied
? ["unsatisfied", "En attente de vos réponses..."]
: ["figure", humanFigure(2)(ruleValue) + " €"]
{
/*<p><i className="fa fa-lightbulb-o" aria-hidden="true"></i><em>Pourquoi ?</em></p> */
}
<ReactCSSTransitionGroup
transitionName="flash"
transitionEnterTimeout={100}
transitionLeaveTimeout={100}
>
<span key={text} className="rule-value">
{" "}
{conversationStarted && <span className={className}>{text}</span>}
</span>
</ReactCSSTransitionGroup>
}

View File

@ -7,7 +7,7 @@ export default forcedThemeColour => {
return script && script.getAttribute('couleur')
},
// Use the default theme colour if the host page hasn't made a choice
defaultColour = '#4A89DC',
defaultColour = '#2975D1',
colour = forcedThemeColour || scriptColour() || defaultColour,
textColour = findContrastedTextColour(colour, true), // the 'simple' version feels better...
inverseTextColour = textColour === '#ffffff' ? '#000' : '#fff',

View File

@ -35,12 +35,6 @@ h1 {
flex-direction: column;
}
#ninetyPercent {
width: 100%;
flex-grow: 1;
overflow: auto;
}
#page-type {
display: inline-block;
position: fixed;

View File

@ -25,7 +25,15 @@ const piwik = new ReactPiwik({
export default class Layout extends Component {
history = createHistory()
state = {
resultsHeight: 600
}
componentDidMount(){
let resultsEl = document.getElementById('results')
this.setState({
resultsHeight: resultsEl ? resultsEl.clientHeight : 600
})
}
render() {
let displayWarning = ["/simu/", "/regle/", "/regles"].find(
t => window.location.href.toString().indexOf(t) > -1
@ -37,7 +45,7 @@ export default class Layout extends Component {
return (
<Router history={piwik.connectToHistory(this.history)}>
<div id="main">
<div id="ninetyPercent">
<div>
<div id="header">
{ displayWarning &&
@ -62,6 +70,7 @@ export default class Layout extends Component {
<Route component={Route404} />
</Switch>
</div>
<div id="antiOverlap" style={{height: this.state.resultsHeight + 'px'}}/>
<Results />
</div>
</Router>