ajoute un bouton pour choisir de reprendre la dernière simulation sauvegarder

pull/239/head
Johan Girod 2018-05-23 18:03:26 +02:00
parent e821247fa7
commit 191bd66a1a
19 changed files with 205 additions and 118 deletions

View File

@ -1,5 +1,8 @@
/* @flow */
import type { ResetSimulationAction } from './types/Actions'
import type {
ResetSimulationAction,
LoadPreviousSimulationAction
} from './types/Actions'
// The input "conversation" is composed of "steps"
// The state keeps track of which of them have been submitted
@ -26,6 +29,11 @@ export const START_CONVERSATION = 'START_CONVERSATION'
export const CHANGE_THEME_COLOUR = 'CHANGE_THEME_COLOUR'
export function loadPreviousSimulation(): LoadPreviousSimulationAction {
return {
type: 'LOAD_PREVIOUS_SIMULATION'
}
}
// $FlowFixMe
export function changeThemeColour(colour) {
return { type: CHANGE_THEME_COLOUR, colour }

View File

@ -1,17 +1,19 @@
.news-header {
.banner {
display: flex;
align-items: center;
max-width: 30em;
margin: 0 auto;
margin-top: 1em;
width: 40em;
justify-content: center;
animation: fade-in 2s;
animation: fade-in 1s;
}
.news-header p {
.banner p {
text-align: center;
color: #4b4b66;
font-size: 85%;
}
.news-header i {
.banner i {
margin-right: 1em;
}

View File

@ -0,0 +1,48 @@
/* @flow */
import React from 'react'
import type { Node } from 'react'
import { connect } from 'react-redux'
import { compose } from 'ramda'
import withColours from './withColours'
import './Banner.css'
import type { State } from '../types/State'
type PropTypes = {
hidden: boolean,
fontAwesomeIcon: string,
children: Node
}
type ConnectedPropTypes = PropTypes & {
colours: { textColourOnWhite: string }
}
let Banner = ({
hidden,
fontAwesomeIcon,
colours: { textColourOnWhite },
children
}: ConnectedPropTypes) =>
!hidden ? (
<div className="banner">
<i
className={`fa fa-${fontAwesomeIcon}`}
aria-hidden="true"
style={{
color: textColourOnWhite
}}
/>
<p>{children}</p>
</div>
) : null
export default compose(
withColours,
connect(
(state: State, { hidden }: PropTypes) => ({
hidden: hidden || state.conversationStarted
}),
{}
)
)(Banner)

View File

@ -1,32 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { compose } from 'ramda'
import withColours from './withColours'
import { Trans, translate } from 'react-i18next'
import './News.css'
let News = ({ hidden, colours: { textColourOnWhite } }) =>
!hidden ? (
<div className="news-header">
<i
className="fa fa-newspaper-o"
aria-hidden="true"
style={{
color: textColourOnWhite
}}
/>
<p>
<Trans i18nKey="news">
Le simulateur vous propose désormais une estimation instantanée !
</Trans>
</p>
</div>
) : null
export default compose(
withColours,
translate(),
connect(state => ({
hidden: state.conversationStarted
}))
)(News)

View File

@ -0,0 +1,33 @@
/* @flow */
import React from 'react'
import type { SavedSimulation } from '../types/State'
import { loadPreviousSimulation } from '../actions'
import Banner from './Banner'
import { connect } from 'react-redux'
import { Trans } from 'react-i18next'
type ConnectedPropTypes = {
previousSimulation: SavedSimulation,
loadPreviousSimulation: () => void
}
const PreviousSimulationBanner = ({
previousSimulation,
loadPreviousSimulation
}: ConnectedPropTypes) => (
<Banner hidden={!previousSimulation}>
<Trans key="previousSimulationBanner">
Votre précédente simulation a été automatiquement sauvegardée.
</Trans>
<button
className="unstyledButton linkButton"
onClick={loadPreviousSimulation}>
<Trans key="previousSimulationBanner.retrieveButton">
Retrouver ma dernière simulation
</Trans>
</button>
</Banner>
)
export default connect(({ previousSimulation }) => ({ previousSimulation }), {
loadPreviousSimulation
})(PreviousSimulationBanner)

View File

@ -1,7 +1,5 @@
#simu {
margin: 0 auto;
margin-top: 1em;
max-width: 40em;
}
#focusZone {
}

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react'
import { translate } from 'react-i18next'
import { pick } from 'ramda'
import Aide from '../Aide'
import { reduxForm, reset } from 'redux-form'
import { reduxForm } from 'redux-form'
import { getInputComponent } from 'Engine/generateQuestions'
import { connect } from 'react-redux'
import './conversation.css'
@ -24,13 +24,7 @@ import './conversation.css'
'analysis',
'flatRules',
'conversationStarted'
]),
dispatch => ({
reinitialise: () => {
dispatch(reset('conversation'))
dispatch({ type: 'SET_CONVERSATION_TARGETS', reset: true })
}
})
])
)
export default class Conversation extends Component {
render() {

View File

@ -30,8 +30,8 @@ import { scroller, Element, animateScroll } from 'react-scroll'
@translate()
export default class FoldedSteps extends Component {
handleSimulationReset = () => {
this.props.resetForm()
this.props.resetSimulation()
this.props.resetForm()
}
render() {
let {
@ -97,8 +97,8 @@ export class GoToAnswers extends Component {
<h3
className="scrollIndication up"
style={{
opacity: this.props.foldedSteps.length != 0 ? 1 : 0,
color: this.props.colours.textColourOnWhite
color: this.props.colours.textColourOnWhite,
visibility: !this.props.foldedSteps.length ? 'hidden' : 'visible'
}}>
<button
className="unstyledButton"

View File

@ -2,14 +2,13 @@ import React from 'react'
import './Pages.css'
import './Home.css'
import Simu from '../Simu'
import News from '../News'
import PreviousSimulationBanner from '../PreviousSimulationBanner'
export default () => (
const Home = () => (
<div id="home" className="page">
{/* Use this News component to talk about things that are not naturally discoverable */}
{/* <News /> */}
<PreviousSimulationBanner />
<Simu />
<a href="https://beta.gouv.fr" target="_blank">
<a href="https://beta.gouv.fr" target="_blank" rel="noopener noreferrer">
<img
id="marianne"
src={require('Images/marianne.svg')}
@ -18,3 +17,5 @@ export default () => (
</a>
</div>
)
export default Home

View File

@ -56,3 +56,12 @@ h2 {
border-radius: 0;
font-size: inherit;
}
.linkButton {
color: rgb(41, 117, 209);
text-decoration: underline;
}
.linkButton:hover {
color: rgb(21, 97, 189);
}

View File

@ -6,7 +6,10 @@ import DevTools from './DevTools'
import { Provider } from 'react-redux'
import Layout from './containers/Layout'
import { AppContainer } from 'react-hot-loader'
import { persistState, retrievePersistedState } from './storage/persist'
import {
persistSimulation,
retrievePersistedSimulation
} from './storage/persist'
import debounceFormChangeActions from './debounceFormChangeActions'
import computeThemeColours from './components/themeColours'
import { getIframeOption, getUrl } from './utils'
@ -17,7 +20,7 @@ import lang from './i18n'
let initialState = {
iframe: getUrl().includes('iframe'),
themeColours: computeThemeColours(getIframeOption('couleur')),
...retrievePersistedState()
previousSimulation: retrievePersistedSimulation()
}
let enhancer = compose(
@ -34,7 +37,7 @@ let initialRules = lang == 'en' ? rules : rulesFr
let store = createStore(reducers(tracker, initialRules), initialState, enhancer)
let anchor = document.querySelector('#js')
persistState(store)
persistSimulation(store)
let App = ({ store }) => (
<Provider store={store}>

View File

@ -1,5 +1,6 @@
enterSalary: Enter a monthly salary
news: Get a salary estimate in seconds, describe your situation for more accuracy.
previousSimulationBanner: Your previous simulation data have been saved.
previousSimulationBanner.retrieveButton: Retrieve my last simulation
Estimation approximative: This is an estimate
defaults: for a permanent (CDI) full-time contract
Affiner le calcul: Refine this estimate

View File

@ -44,7 +44,6 @@ export default (tracker, flatRules, answerSource) => (state, action) => {
if (path(['form', 'conversation', 'syncErrors'], state)) return state
console.log('before: ' + action.type, answerSource(state))
// Most rules have default values
let rulesDefaults = collectDefaults(flatRules),
situationWithDefaults = assume(answerSource, rulesDefaults)

View File

@ -3,6 +3,8 @@ import reduceReducers from 'reduce-reducers'
import { reducer as formReducer, formValueSelector } from 'redux-form'
import reduceSteps from './reduceSteps'
import computeThemeColours from 'Components/themeColours'
import storageReducer from '../storage/reducer'
import { defaultTo, always } from 'ramda'
import { formatInputs } from 'Engine/rules'
import { popularTargetNames } from 'Components/TargetSelection'
@ -33,7 +35,6 @@ function currentExample(state = null, { type, situation, name }) {
function conversationStarted(state = false, { type }) {
switch (type) {
case 'START_CONVERSATION':
case 'LOAD_PREVIOUS_SIMULATION':
return true
case 'RESET_SIMULATION':
return false
@ -70,31 +71,32 @@ function analysis(state = null, { type }) {
}
export default (tracker, initialRules) =>
reduceReducers(
storageReducer,
combineReducers({
sessionId: (id = Math.floor(Math.random() * 1000000000000) + '') => id,
sessionId: defaultTo(Math.floor(Math.random() * 1000000000000) + ''),
// this is handled by redux-form, pas touche !
form: formReducer,
/* Have forms been filled or ignored ?
false means the user is reconsidering its previous input */
foldedSteps,
currentQuestion: (state = null) => state,
nextSteps: (state = []) => state,
missingVariablesByTarget: (state = {}) => state,
parsedRules: (state = null) => state,
flatRules: (state = null) => state,
currentQuestion: defaultTo(null),
nextSteps: defaultTo([]),
missingVariablesByTarget: defaultTo({}),
parsedRules: defaultTo(null),
flatRules: defaultTo(null),
analysis,
targetNames: (state = popularTargetNames) => state,
targetNames: defaultTo(popularTargetNames),
situationGate: (state = () => null) => state,
situationGate: defaultTo(always(null)),
iframe: (state = false) => state,
iframe: defaultTo(false),
themeColours,
explainedVariable,
previousSimulation: defaultTo(null),
currentExample,
conversationStarted,

View File

@ -2,7 +2,7 @@
import type { Store } from 'redux'
import { serialize, deserialize } from './serialize'
import type { State } from '../types/State'
import type { State, SavedSimulation } from '../types/State'
import type { Action } from '../types/Actions'
const VERSION = 1
@ -17,7 +17,7 @@ function throttle(timeout: number, fn: () => void): () => void {
const LOCAL_STORAGE_KEY = 'embauche.gouv.fr::persisted-simulation::v' + VERSION
export function persistState(store: Store<State, Action>) {
export function persistSimulation(store: Store<State, Action>) {
const listener = () => {
const state = store.getState()
if (!state.conversationStarted) {
@ -25,15 +25,10 @@ export function persistState(store: Store<State, Action>) {
}
window.localStorage.setItem(LOCAL_STORAGE_KEY, serialize(state))
}
if (retrievePersistedState()) {
store.dispatch({
type: 'LOAD_PREVIOUS_SIMULATION'
})
}
store.subscribe(throttle(1000, listener))
}
export function retrievePersistedState(): ?State {
export function retrievePersistedSimulation(): ?SavedSimulation {
const serializedState = window.localStorage.getItem(LOCAL_STORAGE_KEY)
return serializedState ? deserialize(serializedState) : null
}

25
source/storage/reducer.js Normal file
View File

@ -0,0 +1,25 @@
/* @flow */
import type { State } from '../types/State'
import type { Action } from '../types/Actions'
import {
currentSimulationSelector,
createStateFromSavedSimulation
} from './selectors'
export default (state: State, action: Action): State => {
switch (action.type) {
case 'LOAD_PREVIOUS_SIMULATION':
return {
...state,
...createStateFromSavedSimulation(state.previousSimulation)
}
case 'RESET_SIMULATION':
return {
...state,
previousSimulation: currentSimulationSelector(state)
}
default:
return state
}
}

View File

@ -0,0 +1,27 @@
/* @flow */
import type { Situation } from '../types/Situation.js'
import type { SavedSimulation, State } from '../types/State.js'
const situationSelector: State => Situation = state =>
state.form.conversation.values
export const currentSimulationSelector: State => SavedSimulation = state => ({
situation: situationSelector(state),
activeTargetInput: state.activeTargetInput,
foldedSteps: state.foldedSteps
})
export const createStateFromSavedSimulation: (
?SavedSimulation
) => ?State = simulation =>
simulation && {
activeTargetInput: simulation.activeTargetInput,
form: {
conversation: {
values: simulation.situation
}
},
foldedSteps: simulation.foldedSteps,
conversationStarted: true,
previousSimulation: null
}

View File

@ -1,44 +1,11 @@
/* @flow */
import type { Situation } from '../types/Situation.js'
import type { TargetInput, State } from '../types/State.js'
import type { State, SavedSimulation } from '../types/State.js'
import { pipe } from 'ramda'
type PersistedState = {
situation: Situation,
activeTargetInput: TargetInput,
foldedSteps: Array<string>
}
const situationSelector: State => Situation = state =>
state.form.conversation.values
const persistedStateSelector: State => PersistedState = state => ({
situation: situationSelector(state),
activeTargetInput: state.activeTargetInput,
foldedSteps: state.foldedSteps
})
const createStateFromPersistedState: PersistedState => State = ({
activeTargetInput,
situation,
foldedSteps
}) => ({
activeTargetInput,
form: {
conversation: {
values: situation
}
},
foldedSteps,
conversationStarted: true
})
import { currentSimulationSelector } from './selectors'
export const serialize: State => string = pipe(
persistedStateSelector,
currentSimulationSelector,
JSON.stringify
)
export const deserialize: string => State = pipe(
JSON.parse,
createStateFromPersistedState
)
export const deserialize: string => SavedSimulation = JSON.parse

View File

@ -6,12 +6,19 @@ export type TargetInput =
| 'contrat salarié . salaire total'
| 'contrat salarié . salaire de net'
export type SavedSimulation = {
situation: Situation,
activeTargetInput: TargetInput,
foldedSteps: Array<string>
}
export type State = {
form: {
conversation: {
values: Situation
}
},
previousSimulation: ?SavedSimulation,
foldedSteps: Array<string>,
activeTargetInput: TargetInput,
conversationStarted: boolean