✨ ajoute un bouton pour choisir de reprendre la dernière simulation sauvegarder
parent
e821247fa7
commit
191bd66a1a
|
@ -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 }
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -1,7 +1,5 @@
|
|||
#simu {
|
||||
margin: 0 auto;
|
||||
margin-top: 1em;
|
||||
max-width: 40em;
|
||||
}
|
||||
|
||||
#focusZone {
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue