parent
4c5cf52bd8
commit
e8072fe8e1
|
@ -129,11 +129,6 @@
|
|||
status = 200
|
||||
|
||||
# Mon-entreprise.fr PRODUCTION settings
|
||||
[[redirects]]
|
||||
from = "https://mon-entreprise.fr/stats"
|
||||
to = "https://mon-entreprise.glitch.me/"
|
||||
status = 200
|
||||
|
||||
[[redirects]]
|
||||
from = "https://mon-entreprise.fr/robots.txt"
|
||||
to = "/robots.infrance.txt"
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
"react-syntax-highlighter": "^10.1.1",
|
||||
"react-to-print": "^2.5.1",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"recharts": "^1.8.5",
|
||||
"reduce-reducers": "^1.0.4",
|
||||
"redux": "^4.0.4",
|
||||
"redux-thunk": "^2.3.0",
|
||||
|
@ -130,6 +131,7 @@
|
|||
"@types/react-router-dom": "^5.1.0",
|
||||
"@types/react-router-hash-link": "^1.2.1",
|
||||
"@types/react-syntax-highlighter": "^11.0.4",
|
||||
"@types/recharts": "^1.8.9",
|
||||
"@types/styled-components": "^4.1.19",
|
||||
"@types/webpack": "^4.41.10",
|
||||
"@types/webpack-env": "^1.14.1",
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import Value from 'Components/Value'
|
||||
import React, { useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { animated, config, useSpring } from 'react-spring'
|
||||
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
|
||||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
|
||||
const ANIMATION_SPRING = config.gentle
|
||||
|
||||
let ChartItemBar = ({ styles, color, numberToPlot, unit }) => (
|
||||
<div className="distribution-chart__bar-container">
|
||||
<animated.div
|
||||
className="distribution-chart__bar"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
flex: styles.flex
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
font-weight: bold;
|
||||
margin-left: 1rem;
|
||||
color: var(--textColorOnWhite);
|
||||
`}
|
||||
>
|
||||
<Value maximumFractionDigits={0} unit={unit}>
|
||||
{numberToPlot}
|
||||
</Value>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
let BranchIcône = ({ icône }) => (
|
||||
<div className="distribution-chart__legend">
|
||||
<span className="distribution-chart__icon">{emoji(icône)}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
type BarChartBranchProps = {
|
||||
value: number
|
||||
title: React.ReactNode
|
||||
icon?: string
|
||||
maximum: number
|
||||
description?: string
|
||||
unit?: string
|
||||
}
|
||||
|
||||
export default function BarChartBranch({
|
||||
value,
|
||||
title,
|
||||
icon,
|
||||
maximum,
|
||||
description,
|
||||
unit
|
||||
}: BarChartBranchProps) {
|
||||
const [intersectionRef, brancheInViewport] = useDisplayOnIntersecting({
|
||||
threshold: 0.5
|
||||
})
|
||||
const { color } = useContext(ThemeColorsContext)
|
||||
const numberToPlot = brancheInViewport ? value : 0
|
||||
const styles = useSpring({
|
||||
config: ANIMATION_SPRING,
|
||||
to: {
|
||||
flex: numberToPlot / maximum,
|
||||
opacity: numberToPlot ? 1 : 0
|
||||
}
|
||||
}) as { flex: number; opacity: number } // TODO: problème avec les types de react-spring ?
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
ref={intersectionRef}
|
||||
className="distribution-chart__item"
|
||||
style={{ opacity: styles.opacity }}
|
||||
>
|
||||
{icon && <BranchIcône icône={icon} />}
|
||||
<div className="distribution-chart__item-content">
|
||||
<p className="distribution-chart__counterparts">
|
||||
<span className="distribution-chart__branche-name">{title}</span>
|
||||
<br />
|
||||
{description && <small>{description}</small>}
|
||||
</p>
|
||||
<ChartItemBar
|
||||
{...{
|
||||
styles,
|
||||
color,
|
||||
numberToPlot,
|
||||
unit
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</animated.div>
|
||||
)
|
||||
}
|
|
@ -1,16 +1,12 @@
|
|||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import useDisplayOnIntersecting from 'Components/utils/useDisplayOnIntersecting'
|
||||
import Value from 'Components/Value'
|
||||
import React, { useContext } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import React from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { animated, config, useSpring } from 'react-spring'
|
||||
import { DottedName } from 'Rules'
|
||||
import { parsedRulesSelector } from 'Selectors/analyseSelectors'
|
||||
import répartitionSelector from 'Selectors/repartitionSelectors'
|
||||
import './Distribution.css'
|
||||
import './PaySlip'
|
||||
import RuleLink from './RuleLink'
|
||||
import BarChartBranch from './BarChart'
|
||||
|
||||
export default function Distribution() {
|
||||
const distribution = useSelector(répartitionSelector) as any
|
||||
|
@ -28,7 +24,7 @@ export default function Distribution() {
|
|||
key={brancheDottedName}
|
||||
dottedName={brancheDottedName}
|
||||
value={partPatronale + partSalariale}
|
||||
distribution={distribution}
|
||||
maximum={distribution.maximum}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
@ -40,91 +36,26 @@ export default function Distribution() {
|
|||
type DistributionBranchProps = {
|
||||
dottedName: DottedName
|
||||
value: number
|
||||
distribution: { maximum: number; total: number }
|
||||
maximum: number
|
||||
icon?: string
|
||||
}
|
||||
|
||||
const ANIMATION_SPRING = config.gentle
|
||||
export function DistributionBranch({
|
||||
dottedName,
|
||||
value,
|
||||
icon,
|
||||
distribution
|
||||
maximum
|
||||
}: DistributionBranchProps) {
|
||||
const rules = useSelector(parsedRulesSelector)
|
||||
const [intersectionRef, brancheInViewport] = useDisplayOnIntersecting({
|
||||
threshold: 0.5
|
||||
})
|
||||
const { color } = useContext(ThemeColorsContext)
|
||||
const branche = rules[dottedName]
|
||||
const montant = brancheInViewport ? value : 0
|
||||
const styles = useSpring({
|
||||
config: ANIMATION_SPRING,
|
||||
to: {
|
||||
flex: montant / distribution.maximum,
|
||||
opacity: montant ? 1 : 0
|
||||
}
|
||||
}) as { flex: number; opacity: number } // TODO: problème avec les types de react-spring ?
|
||||
|
||||
const branch = rules[dottedName]
|
||||
return (
|
||||
<animated.div
|
||||
ref={intersectionRef}
|
||||
className="distribution-chart__item"
|
||||
style={{ opacity: styles.opacity }}
|
||||
>
|
||||
<BranchIcône icône={(icon ?? branche.icons) as string} />
|
||||
<div className="distribution-chart__item-content">
|
||||
<p className="distribution-chart__counterparts">
|
||||
<span className="distribution-chart__branche-name">
|
||||
<RuleLink {...branche} />
|
||||
</span>
|
||||
<br />
|
||||
<small>{branche.summary}</small>
|
||||
</p>
|
||||
<ChartItemBar
|
||||
{...{
|
||||
styles,
|
||||
color,
|
||||
montant,
|
||||
total: distribution.total
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</animated.div>
|
||||
<BarChartBranch
|
||||
value={value}
|
||||
maximum={maximum}
|
||||
title={<RuleLink {...branch} />}
|
||||
icon={icon ?? branch.icons}
|
||||
description={branch.summary}
|
||||
unit="€"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
type ChartItemBarProps = {
|
||||
styles: React.CSSProperties
|
||||
color: string
|
||||
montant: number
|
||||
}
|
||||
|
||||
let ChartItemBar = ({ styles, color, montant }: ChartItemBarProps) => (
|
||||
<div className="distribution-chart__bar-container">
|
||||
<animated.div
|
||||
className="distribution-chart__bar"
|
||||
style={{
|
||||
backgroundColor: color,
|
||||
...styles
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
css={`
|
||||
font-weight: bold;
|
||||
margin-left: 1rem;
|
||||
color: var(--textColorOnWhite);
|
||||
`}
|
||||
>
|
||||
<Value maximumFractionDigits={0} unit="€">
|
||||
{montant}
|
||||
</Value>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
let BranchIcône = ({ icône }: { icône: string }) => (
|
||||
<div className="distribution-chart__legend">
|
||||
<span className="distribution-chart__icon">{emoji(icône)}</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -34,17 +34,16 @@ export default function MoreInfosOnUs() {
|
|||
<div className="ui__ small simple button">Découvrir</div>
|
||||
</Link>
|
||||
)}
|
||||
<a
|
||||
href="https://mon-entreprise.fr/stats"
|
||||
className="ui__ interactive card box"
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji('📊')}</div>
|
||||
<h3>Les statistiques</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Quel est notre impact ?
|
||||
</p>
|
||||
<div className="ui__ small simple button">Découvrir</div>
|
||||
</a>
|
||||
{!pathname.startsWith(sitePaths.stats) && (
|
||||
<Link className="ui__ interactive card box" to={sitePaths.stats}>
|
||||
<div className="ui__ big box-icon">{emoji('📊')}</div>
|
||||
<h3>Les statistiques</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Quel est notre impact ?
|
||||
</p>
|
||||
<div className="ui__ small simple button">Découvrir</div>
|
||||
</Link>
|
||||
)}
|
||||
{!pathname.startsWith(sitePaths.budget) && (
|
||||
<Link className="ui__ interactive card box" to={sitePaths.budget}>
|
||||
<div className="ui__ big box-icon">{emoji('💶')}</div>
|
||||
|
|
|
@ -81,15 +81,19 @@ export function roundedPercentages(values: Array<number>) {
|
|||
}
|
||||
|
||||
type StackedBarChartProps = {
|
||||
data: Array<{ color?: string } & EvaluatedRule<DottedName>>
|
||||
data: Array<{
|
||||
color?: string
|
||||
value: number | undefined
|
||||
legend: React.ReactNode
|
||||
key: string
|
||||
}>
|
||||
}
|
||||
|
||||
export default function StackedBarChart({ data }: StackedBarChartProps) {
|
||||
export function StackedBarChart({ data }: StackedBarChartProps) {
|
||||
const [intersectionRef, displayChart] = useDisplayOnIntersecting({
|
||||
threshold: 0.5
|
||||
})
|
||||
data = data.filter(d => d.nodeValue != undefined)
|
||||
const percentages = roundedPercentages(data.map(d => d.nodeValue as number))
|
||||
const percentages = roundedPercentages(data.map(d => d.value ?? 0))
|
||||
const dataWithPercentage = data.map((data, index) => ({
|
||||
...data,
|
||||
percentage: percentages[index]
|
||||
|
@ -103,21 +107,21 @@ export default function StackedBarChart({ data }: StackedBarChartProps) {
|
|||
// <BarItem /> has a border so we don't want to display empty bars
|
||||
// (even with width 0).
|
||||
.filter(({ percentage }) => percentage !== 0)
|
||||
.map(({ dottedName, color, percentage }) => (
|
||||
.map(({ key, color, percentage }) => (
|
||||
<BarItem
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
backgroundColor: color || 'green'
|
||||
}}
|
||||
key={dottedName}
|
||||
key={key}
|
||||
/>
|
||||
))}
|
||||
</BarStack>
|
||||
<BarStackLegend>
|
||||
{dataWithPercentage.map(({ percentage, color, ...rule }) => (
|
||||
<BarStackLegendItem key={rule.dottedName}>
|
||||
{dataWithPercentage.map(({ key, percentage, color, legend }) => (
|
||||
<BarStackLegendItem key={key}>
|
||||
<SmallCircle style={{ backgroundColor: color }} />
|
||||
<RuleLink {...rule}>{capitalise0(rule.title)}</RuleLink>
|
||||
<strong>{legend}</strong>
|
||||
<strong>{percentage} %</strong>
|
||||
</BarStackLegendItem>
|
||||
))}
|
||||
|
@ -125,3 +129,20 @@ export default function StackedBarChart({ data }: StackedBarChartProps) {
|
|||
</animated.div>
|
||||
)
|
||||
}
|
||||
|
||||
type StackedRulesChartProps = {
|
||||
data: Array<{ color?: string } & EvaluatedRule<DottedName>>
|
||||
}
|
||||
|
||||
export default function StackedRulesChart({ data }: StackedRulesChartProps) {
|
||||
return (
|
||||
<StackedBarChart
|
||||
data={data.map(rule => ({
|
||||
...rule,
|
||||
key: rule.dottedName,
|
||||
value: rule.nodeValue,
|
||||
legend: <RuleLink {...rule}>{capitalise0(rule.title)}</RuleLink>
|
||||
}))}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -50,7 +50,8 @@ const deriveAnalogousPalettes = (hex: string) => {
|
|||
const [h, s, l] = convert.hex.hsl(hex.split('#')[1])
|
||||
return [
|
||||
generateDarkenVariations(4, [(h - 45) % 360, 0.75 * s, l]),
|
||||
generateDarkenVariations(4, [(h + 45) % 360, 0.75 * s, l])
|
||||
generateDarkenVariations(4, [(h + 45) % 360, 0.75 * s, l]),
|
||||
generateDarkenVariations(4, [(h + 90) % 360, 0.75 * s, l])
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ export function normalizeDateString(dateString: string): string {
|
|||
}
|
||||
return normalizeDate(+year, +month, +day)
|
||||
}
|
||||
|
||||
const pad = (n: number): string => (+n < 10 ? `0${n}` : '' + n)
|
||||
export function normalizeDate(
|
||||
year: number,
|
||||
|
|
|
@ -8,6 +8,7 @@ Accueil: Home
|
|||
Aide à la déclaration de revenus au titre de l'année 2019: Help with your 2019 income tax return
|
||||
Alors: Then
|
||||
Année d'activité: Years of activity
|
||||
Artiste-auteur: Artist-author
|
||||
Assimilé salarié: '"Assimilé-salarié"'
|
||||
Au-delà du dernier plafond: Beyond the last ceiling
|
||||
Au-dessus de: Above
|
||||
|
@ -24,7 +25,9 @@ Choisir plus tard: Choose later
|
|||
Code d'intégration: Integration Code
|
||||
Commencer: Get started
|
||||
'Commerçant, artisan, ou libéral ?': 'Trader, craftsman, or liberal?'
|
||||
Comparaison statuts: Status comparison
|
||||
Continuer: Continue
|
||||
Coronavirus: Coronavirus
|
||||
Cotisations: Contributions
|
||||
Cotisations sociales: Social contributions
|
||||
'Covid-19 : Découvrez les mesures de soutien aux entreprises': 'Covid-19: Find out about business support measures'
|
||||
|
@ -61,8 +64,8 @@ Gérant minoritaire: Managing director
|
|||
Habituellement: Usually
|
||||
Imprimer: Print
|
||||
Impôts: Taxes
|
||||
'Indemnité chômage partiel prise en charge par l''état :': 'State-paid short-time working allowance :'
|
||||
Indépendant: Independent
|
||||
"Indemnité chômage partiel prise en charge par l'état :": 'State-paid short-time working allowance :'
|
||||
Indépendant: 'Indépendant'
|
||||
International: International
|
||||
Intégrer l'interface de simulation: Integrate the simulation interface
|
||||
Intégrer la bibliothèque de calcul: Integrate the calculation library
|
||||
|
@ -78,7 +81,7 @@ Mon entreprise: My company
|
|||
Mon revenu: My income
|
||||
Montant: Amount
|
||||
Montant des cotisations: Amount of contributions
|
||||
'Nom de l''entreprise ou SIREN ': Company name or SIREN code
|
||||
"Nom de l'entreprise ou SIREN ": Company name or SIREN code
|
||||
Non: 'No'
|
||||
Nous n'avons rien trouvé: We didn't find any matching registered company.
|
||||
Oui: 'Yes'
|
||||
|
@ -132,6 +135,7 @@ Saisissez votre domaine d'activité: Enter your business area
|
|||
Salaire: Salary
|
||||
Salaire net: Net Salary
|
||||
Salaire net et brut: Net and gross salary
|
||||
Salarié: Employee
|
||||
Sans responsabilité limitée: Without limited liability
|
||||
Si: If
|
||||
Simulateur de salaire: Employee salary simulation
|
||||
|
@ -143,7 +147,7 @@ Taux: Rate
|
|||
Taux calculé: Calculated rate
|
||||
Taux moyen: Average rate
|
||||
Total des retenues: Total withheld
|
||||
'Total payé par l''entreprise :': 'Total paid by the company :'
|
||||
"Total payé par l'entreprise :": 'Total paid by the company :'
|
||||
Tout effacer: Delete all
|
||||
Tranche de l'assiette: Scale bracket
|
||||
Un seul associé: Only one partner
|
||||
|
@ -927,6 +931,7 @@ path:
|
|||
index: /simulators
|
||||
indépendant: /independant
|
||||
salarié: /salaried
|
||||
stats: /stats
|
||||
économieCollaborative:
|
||||
index: /sharing-economy
|
||||
votreSituation: /your-situation
|
||||
|
@ -1019,20 +1024,6 @@ simlateurs:
|
|||
the year 2020 based on your projected income.
|
||||
simulateurs:
|
||||
accueil:
|
||||
artiste-auteur: >-
|
||||
<0>Artist-author</0><1>Estimating the social security contributions of an
|
||||
artist or author</1>
|
||||
assimilé: |
|
||||
<0>"Assimilé-salarié"</0>
|
||||
<1>Calculate the income of an officer of a minority SAS, SASU or SARL</1>
|
||||
auto: |
|
||||
<0>Auto-entrepreneur</0>
|
||||
<1>Calculate the income (or turnover) of an auto-entrepreneur</1>
|
||||
comparaison: >
|
||||
<0>Status comparison</0>
|
||||
|
||||
<1>Simulate the differences between the plans (contributions, retirement,
|
||||
maternity, illness, etc.)</1>
|
||||
description: >-
|
||||
<0>All the simulators on this site are:</0>
|
||||
|
||||
|
@ -1040,14 +1031,6 @@ simulateurs:
|
|||
to increase the number of devices taken into account</1> <2>Developed in
|
||||
partnership with the Urssaf (the contribution collector entity in
|
||||
France)<2> </1>
|
||||
indépendant: |
|
||||
<0>"Indépendant"</0>
|
||||
<1>Calculate the income of a majority manager of EURL, EI, or SARL</1>
|
||||
salarié: >
|
||||
<0>Employee</0>
|
||||
|
||||
<1>Calculate the net, gross, or total salary of an employee, trainee, or
|
||||
similar</1>
|
||||
titre: Available simulators
|
||||
artiste-auteur:
|
||||
titre: Estimate my artist/author contributions
|
||||
|
@ -1099,6 +1082,20 @@ simulateurs:
|
|||
défaut: 'Refine the simulation by answering the following questions:'
|
||||
faible: Low accuracy
|
||||
moyenne: Medium accuracy
|
||||
résumé:
|
||||
artiste-auteur: Estimating the social security contributions of an artist or author
|
||||
assimilé: |
|
||||
Calculate the income of an officer of a minority SAS, SASU or SARL
|
||||
auto: |
|
||||
Calculate the income (or turnover) of an auto-entrepreneur
|
||||
comparaison: >
|
||||
Simulate the differences between the plans (contributions, retirement,
|
||||
maternity, illness, etc.)
|
||||
indépendant: |
|
||||
Calculate the income of a majority manager of EURL, EI, or SARL
|
||||
salarié: >
|
||||
Calculate the net, gross, or total salary of an employee, trainee, or
|
||||
similar
|
||||
salarié:
|
||||
page:
|
||||
description: >-
|
||||
|
|
|
@ -9,8 +9,7 @@
|
|||
// "public repo" authorization when generating the access token.
|
||||
require('dotenv').config()
|
||||
require('isomorphic-fetch')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
var { createDataDir, writeInDataDir } = require('./utils.js')
|
||||
|
||||
// We use the GitHub API V4 in GraphQL to download the releases. A GraphQL
|
||||
// explorer can be found here : https://developer.github.com/v4/explorer/
|
||||
|
@ -48,17 +47,15 @@ const fakeData = [
|
|||
}
|
||||
]
|
||||
|
||||
const dataDir = path.resolve(__dirname, '../data/')
|
||||
|
||||
async function main() {
|
||||
createDataDir()
|
||||
writeReleasesInDataDir(await fetchReleases())
|
||||
}
|
||||
|
||||
function createDataDir() {
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir)
|
||||
}
|
||||
const releases = await fetchReleases()
|
||||
// The last release name is fetched on all pages (to display the banner)
|
||||
// whereas the full release data is used only in the dedicated page, that why
|
||||
// we deduplicate the releases data in two separated files that can be
|
||||
// bundled/fetched separately.
|
||||
writeInDataDir('releases.json', releases)
|
||||
writeInDataDir('last-release.json', { lastRelease: releases[0].name })
|
||||
}
|
||||
|
||||
async function fetchReleases() {
|
||||
|
@ -84,19 +81,4 @@ async function fetchReleases() {
|
|||
}
|
||||
}
|
||||
|
||||
function writeReleasesInDataDir(releases) {
|
||||
// The last release name is fetched on all pages (to display the banner)
|
||||
// whereas the full release data is used only in the dedicated page, that why
|
||||
// we deduplicate the releases data in two separated files that can be
|
||||
// bundled/fetched separately.
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'releases.json'),
|
||||
JSON.stringify(releases)
|
||||
)
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, 'last-release.json'),
|
||||
JSON.stringify({ lastRelease: releases[0].name })
|
||||
)
|
||||
}
|
||||
|
||||
main()
|
||||
|
|
|
@ -0,0 +1,333 @@
|
|||
// This script uses the Matomo API which requires an access token
|
||||
// Once you have your access token you can put it in a `.env` file at the root
|
||||
// of the project to enable it during development. For instance:
|
||||
//
|
||||
// MATOMO_TOKEN=f4336c82cb1e494752d06e610614eab12b65f1d1
|
||||
//
|
||||
// Matomo API documentation:
|
||||
// https://developer.matomo.org/api-reference/reporting-api
|
||||
|
||||
require('dotenv').config()
|
||||
require('isomorphic-fetch')
|
||||
const querystring = require('querystring')
|
||||
const { createDataDir, writeInDataDir } = require('./utils.js')
|
||||
const R = require('ramda')
|
||||
|
||||
const apiURL = params => {
|
||||
const query = querystring.stringify({
|
||||
period: 'month',
|
||||
date: 'last1',
|
||||
method: 'API.get',
|
||||
format: 'JSON',
|
||||
module: 'API',
|
||||
idSite: 39,
|
||||
language: 'fr',
|
||||
apiAction: 'get',
|
||||
token_auth: process.env.MATOMO_TOKEN,
|
||||
...params
|
||||
})
|
||||
return `https://stats.data.gouv.fr/index.php?${query}`
|
||||
}
|
||||
|
||||
async function main() {
|
||||
createDataDir()
|
||||
const stats = {
|
||||
simulators: await fetchSimulatorsMonth(),
|
||||
monthlyVisits: await fetchMonthlyVisits(),
|
||||
dailyVisits: await fetchDailyVisits(),
|
||||
statusChosen: await fetchStatusChosen(),
|
||||
feedback: await fetchFeedback(),
|
||||
channelType: await fetchChannelType()
|
||||
}
|
||||
writeInDataDir('stats.json', stats)
|
||||
}
|
||||
|
||||
function xMonthAgo(x = 0) {
|
||||
const date = new Date()
|
||||
if (date.getMonth() - x > 0) {
|
||||
date.setMonth(date.getMonth() - x)
|
||||
} else {
|
||||
date.setMonth(12 + date.getMonth() - x)
|
||||
date.setFullYear(date.getFullYear() - 1)
|
||||
}
|
||||
return date.toISOString().substring(0, 7)
|
||||
}
|
||||
|
||||
async function fetchSimulatorsMonth() {
|
||||
const getDataFromXMonthAgo = async x => {
|
||||
const date = xMonthAgo(x)
|
||||
return { date, visites: await fetchSimulators(`${date}-01`) }
|
||||
}
|
||||
return {
|
||||
currentMonth: await getDataFromXMonthAgo(0),
|
||||
oneMonthAgo: await getDataFromXMonthAgo(1),
|
||||
twoMonthAgo: await getDataFromXMonthAgo(2)
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchSimulators(dt) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
apiURL({
|
||||
period: 'month',
|
||||
date: `${dt}`,
|
||||
method: 'Actions.getPageUrls',
|
||||
filter_limits: -1
|
||||
})
|
||||
)
|
||||
const firstLevelData = await response.json()
|
||||
|
||||
const coronavirusPage = firstLevelData.find(
|
||||
page => page.label === '/coronavirus'
|
||||
)
|
||||
|
||||
// Visits on simulators pages
|
||||
const idSubTableSimulateurs = firstLevelData.find(
|
||||
page => page.label === 'simulateurs'
|
||||
).idsubdatatable
|
||||
|
||||
const responseSimulateurs = await fetch(
|
||||
apiURL({
|
||||
date: `${dt}`,
|
||||
method: 'Actions.getPageUrls',
|
||||
search_recursive: 1,
|
||||
filter_limits: -1,
|
||||
idSubtable: idSubTableSimulateurs
|
||||
})
|
||||
)
|
||||
|
||||
const dataSimulateurs = await responseSimulateurs.json()
|
||||
const resultSimulateurs = dataSimulateurs
|
||||
.filter(({ label }) =>
|
||||
[
|
||||
'/salarié',
|
||||
'/auto-entrepreneur',
|
||||
'/artiste-auteur',
|
||||
'/indépendant',
|
||||
'/comparaison-régimes-sociaux',
|
||||
'/assimilé-salarié'
|
||||
].includes(label)
|
||||
)
|
||||
|
||||
/// Two '/salarié' pages are reported on Matomo, one of which has very few
|
||||
/// visitors. We delete it manually.
|
||||
.filter(
|
||||
x =>
|
||||
x.label != '/salarié' ||
|
||||
x.nb_visits !=
|
||||
dataSimulateurs
|
||||
.filter(x => x.label == '/salarié')
|
||||
.reduce((a, b) => Math.min(a, b.nb_visits), 1000)
|
||||
)
|
||||
|
||||
// Add iframes
|
||||
const idTableIframes = firstLevelData.find(page => page.label == 'iframes')
|
||||
.idsubdatatable
|
||||
const responseIframes = await fetch(
|
||||
apiURL({
|
||||
date: `${dt}`,
|
||||
method: 'Actions.getPageUrls',
|
||||
search_recursive: 1,
|
||||
filter_limits: -1,
|
||||
idSubtable: idTableIframes
|
||||
})
|
||||
)
|
||||
const dataIframes = await responseIframes.json()
|
||||
const resultIframes = dataIframes.filter(x =>
|
||||
[
|
||||
'/simulateur-embauche?couleur=',
|
||||
'/simulateur-autoentrepreneur?couleur='
|
||||
].includes(x.label)
|
||||
)
|
||||
|
||||
const groupSimulateursIframesVisits = ({ label }) =>
|
||||
label.startsWith('/simulateur-embauche')
|
||||
? '/salarié'
|
||||
: label.startsWith('/simulateur-autoentrepreneur')
|
||||
? '/auto-entrepreneur'
|
||||
: label
|
||||
|
||||
const sumVisits = (acc, { nb_visits }) => acc + nb_visits
|
||||
const results = R.reduceBy(
|
||||
sumVisits,
|
||||
0,
|
||||
groupSimulateursIframesVisits,
|
||||
[...resultSimulateurs, ...resultIframes, coronavirusPage].filter(
|
||||
x => x !== undefined
|
||||
)
|
||||
)
|
||||
return Object.entries(results)
|
||||
.map(([label, nb_visits]) => ({ label, nb_visits }))
|
||||
.sort((a, b) => b.nb_visits - a.nb_visits)
|
||||
} catch (e) {
|
||||
console.log('fail to fetch Simulators Visits')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// We had a tracking bug in 2019, in which every click on Safari+iframe counted
|
||||
// as a visit, so the numbers are manually corrected.
|
||||
const visitsIn2019 = {
|
||||
'2019-01': 119541,
|
||||
'2019-02': 99065,
|
||||
'2019-03': 122931,
|
||||
'2019-04': 113454,
|
||||
'2019-05': 118637,
|
||||
'2019-06': 152981,
|
||||
'2019-07': 141079,
|
||||
'2019-08': 127326,
|
||||
'2019-09': 178474,
|
||||
'2019-10': 198260,
|
||||
'2019-11': 174515,
|
||||
'2019-12': 116305
|
||||
}
|
||||
|
||||
async function fetchMonthlyVisits() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
apiURL({
|
||||
period: 'month',
|
||||
date: 'previous12',
|
||||
method: 'VisitsSummary.getUniqueVisitors'
|
||||
})
|
||||
)
|
||||
const data = await response.json()
|
||||
const result = Object.entries({ ...data, ...visitsIn2019 })
|
||||
.sort(([t1], [t2]) => (t1 > t2 ? 1 : -1))
|
||||
.map(([date, visiteurs]) => ({ date, visiteurs }))
|
||||
return result
|
||||
} catch (e) {
|
||||
console.log('fail to fetch Monthly Visits')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchDailyVisits() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
apiURL({
|
||||
period: 'day',
|
||||
date: 'previous30',
|
||||
method: 'VisitsSummary.getUniqueVisitors'
|
||||
})
|
||||
)
|
||||
const data = await response.json()
|
||||
return Object.entries(data).map(([date, visiteurs]) => ({
|
||||
date,
|
||||
visiteurs
|
||||
}))
|
||||
} catch (e) {
|
||||
console.log('fail to fetch Daily Visits')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchStatusChosen() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
apiURL({
|
||||
method: 'Events.getAction',
|
||||
label: 'status chosen',
|
||||
date: 'previous1'
|
||||
})
|
||||
)
|
||||
const data = await response.json()
|
||||
const response2 = await fetch(
|
||||
apiURL({
|
||||
method: 'Events.getNameFromActionId',
|
||||
idSubtable: Object.values(data)[0][0].idsubdatatable,
|
||||
date: 'previous1'
|
||||
})
|
||||
)
|
||||
const data2 = await response2.json()
|
||||
const result = Object.values(data2)[0].map(({ label, nb_visits }) => ({
|
||||
label,
|
||||
nb_visits
|
||||
}))
|
||||
return result
|
||||
} catch (e) {
|
||||
console.log('fail to fetch Status Chosen')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFeedback() {
|
||||
try {
|
||||
const APIcontent = await fetch(
|
||||
apiURL({
|
||||
method: 'Events.getCategory',
|
||||
label: 'Feedback > @rate%20page%20usefulness',
|
||||
date: 'previous5'
|
||||
})
|
||||
)
|
||||
const APIsimulator = await fetch(
|
||||
apiURL({
|
||||
method: 'Events.getCategory',
|
||||
label: 'Feedback > @rate%20simulator',
|
||||
date: 'previous5'
|
||||
})
|
||||
)
|
||||
const feedbackcontent = await APIcontent.json()
|
||||
const feedbacksimulator = await APIsimulator.json()
|
||||
|
||||
let content = 0
|
||||
let simulator = 0
|
||||
let j = 0
|
||||
// The weights are defined by taking the coefficients of an exponential
|
||||
// smoothing with alpha=0.8 and normalizing them. The current month is not
|
||||
// considered.
|
||||
const weights = [0.0015, 0.0076, 0.0381, 0.1905, 0.7623]
|
||||
for (const i in feedbackcontent) {
|
||||
content += feedbackcontent[i][0].avg_event_value * weights[j]
|
||||
simulator += feedbacksimulator[i][0].avg_event_value * weights[j]
|
||||
j += 1
|
||||
}
|
||||
return {
|
||||
content: Math.round(content * 10),
|
||||
simulator: Math.round(simulator * 10)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('fail to fetch feedbacks')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchChannelType() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
apiURL({
|
||||
period: 'month',
|
||||
date: 'last3',
|
||||
method: 'Referrers.getReferrerType'
|
||||
})
|
||||
)
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
const result = R.map(
|
||||
date =>
|
||||
date
|
||||
.filter(x =>
|
||||
['Sites web', 'Moteurs de recherche', 'Entrées directes'].includes(
|
||||
x.label
|
||||
)
|
||||
)
|
||||
.map(({ label, nb_visits }) => ({
|
||||
label,
|
||||
nb_visits
|
||||
})),
|
||||
data
|
||||
)
|
||||
const dates = Object.keys(result).sort((t1, t2) => t1 - t2)
|
||||
return {
|
||||
currentMonth: { date: dates[0], visites: result[dates[0]] },
|
||||
oneMonthAgo: { date: dates[1], visites: result[dates[1]] },
|
||||
twoMonthAgo: { date: dates[2], visites: result[dates[2]] }
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('fail to fetch channel type')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
main()
|
|
@ -1,2 +1,3 @@
|
|||
require('./dottednames.js')
|
||||
require('./fetch-releases.js')
|
||||
require('./fetch-stats.js')
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const dataDir = path.resolve(__dirname, '../data/')
|
||||
|
||||
exports.createDataDir = () => {
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
fs.mkdirSync(dataDir)
|
||||
}
|
||||
}
|
||||
|
||||
exports.writeInDataDir = (filename, data) => {
|
||||
fs.writeFileSync(path.join(dataDir, filename), JSON.stringify(data, null, 2))
|
||||
}
|
|
@ -36,6 +36,7 @@ import Integration from './pages/integration/index'
|
|||
import Landing from './pages/Landing/Landing'
|
||||
import Nouveautés from './pages/Nouveautés/Nouveautés'
|
||||
import Simulateurs from './pages/Simulateurs'
|
||||
import Stats from './pages/Stats/LazyStats'
|
||||
import ÉconomieCollaborative from './pages/ÉconomieCollaborative'
|
||||
import redirects from './redirects'
|
||||
import { constructLocalizedSitePath } from './sitePaths'
|
||||
|
@ -131,6 +132,7 @@ const App = () => {
|
|||
/>
|
||||
<Route path={sitePaths.integration.index} component={Integration} />
|
||||
<Route path={sitePaths.nouveautés} component={Nouveautés} />
|
||||
<Route path={sitePaths.stats} component={Stats} />
|
||||
<Route path={sitePaths.coronavirus} component={Coronavirus} />
|
||||
<Route path={sitePaths.budget} component={Budget} />
|
||||
<Route exact path="/dev/sitemap" component={Sitemap} />
|
||||
|
|
|
@ -66,7 +66,7 @@ const Footer = () => {
|
|||
{' • '}
|
||||
<Link to={sitePaths.nouveautés}>Nouveautés</Link>
|
||||
{' • '}
|
||||
<a href="https://mon-entreprise.fr/stats">Stats</a>
|
||||
<Link to={sitePaths.stats}>Stats</Link>
|
||||
{' • '}
|
||||
<Link to={sitePaths.budget}>Budget</Link>
|
||||
</>
|
||||
|
|
|
@ -2,7 +2,7 @@ import Overlay from 'Components/Overlay'
|
|||
import React, { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
|
||||
export default function Privacy() {
|
||||
export default function Privacy({ label }: { label?: string }) {
|
||||
const [opened, setOpened] = useState(false)
|
||||
const { i18n } = useTranslation()
|
||||
|
||||
|
@ -16,7 +16,7 @@ export default function Privacy() {
|
|||
return (
|
||||
<>
|
||||
<button onClick={handleOpen} className="ui__ link-button">
|
||||
<Trans>Vie privée</Trans>
|
||||
{label ?? <Trans>Vie privée</Trans>}
|
||||
</button>
|
||||
{opened && (
|
||||
<Overlay onClose={handleClose} style={{ textAlign: 'left' }}>
|
||||
|
|
|
@ -205,7 +205,6 @@ function RepartitionCotisations() {
|
|||
value: useRule(branch.dottedName).nodeValue as number
|
||||
}))
|
||||
const maximum = Math.max(...cotisations.map(x => x.value))
|
||||
const total = cotisations.map(x => x.value).reduce((a = 0, b) => a + b)
|
||||
return (
|
||||
<section>
|
||||
<h2>
|
||||
|
@ -215,7 +214,7 @@ function RepartitionCotisations() {
|
|||
{cotisations.map(cotisation => (
|
||||
<DistributionBranch
|
||||
key={cotisation.dottedName}
|
||||
distribution={{ maximum, total }}
|
||||
maximum={maximum}
|
||||
{...cotisation}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -5,9 +5,83 @@ import { Helmet } from 'react-helmet'
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function Simulateurs() {
|
||||
export function useSimulatorsMetadata() {
|
||||
const { t } = useTranslation()
|
||||
const sitePaths = useContext(SitePathsContext)
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
type SimulatorMetaData = {
|
||||
name: string
|
||||
icône: string
|
||||
description?: string
|
||||
sitePath: string
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
name: t('Assimilé salarié'),
|
||||
icône: '☂️',
|
||||
description: t(
|
||||
'simulateurs.résumé.assimilé',
|
||||
"Calculer le revenu d'un dirigeant de SAS, SASU ou SARL minoritaire"
|
||||
),
|
||||
sitePath: sitePaths.simulateurs['assimilé-salarié']
|
||||
},
|
||||
{
|
||||
name: t('Indépendant'),
|
||||
icône: '👩🔧',
|
||||
description: t(
|
||||
'simulateurs.résumé.indépendant',
|
||||
"Calculer le revenu d'un dirigeant de EURL, EI, ou SARL majoritaire"
|
||||
),
|
||||
sitePath: sitePaths.simulateurs.indépendant
|
||||
},
|
||||
{
|
||||
name: t('Auto-entrepreneur'),
|
||||
icône: '🚶♂️',
|
||||
description: t(
|
||||
'simulateurs.résumé.auto',
|
||||
"Calculer le revenu (ou le chiffre d'affaires) d'un auto-entrepreneur"
|
||||
),
|
||||
sitePath: sitePaths.simulateurs['auto-entrepreneur']
|
||||
},
|
||||
{
|
||||
name: t('Salarié'),
|
||||
icône: '🤝',
|
||||
description: t(
|
||||
'simulateurs.résumé.salarié',
|
||||
"Calculer le salaire net, brut, ou total d'un salarié, stagiaire,ou assimilé"
|
||||
),
|
||||
sitePath: sitePaths.simulateurs.salarié
|
||||
},
|
||||
{
|
||||
name: t('Artiste-auteur'),
|
||||
icône: '👩🎨',
|
||||
description: t(
|
||||
'simulateurs.résumé.artiste-auteur',
|
||||
"Estimer les cotisations sociales d'un artiste ou auteur"
|
||||
),
|
||||
sitePath: sitePaths.simulateurs['artiste-auteur']
|
||||
},
|
||||
{
|
||||
name: t('Comparaison statuts'),
|
||||
icône: '📊',
|
||||
description: t(
|
||||
'simulateurs.résumé.comparaison',
|
||||
'Simulez les différences entre les régimes (cotisations,retraite, maternité, maladie, etc.)'
|
||||
),
|
||||
sitePath: sitePaths.simulateurs.comparaison
|
||||
},
|
||||
{
|
||||
name: t('Coronavirus'),
|
||||
icône: '👨🔬',
|
||||
sitePath: sitePaths.coronavirus
|
||||
}
|
||||
] as Array<SimulatorMetaData>
|
||||
}
|
||||
|
||||
export default function Simulateurs() {
|
||||
const { t } = useTranslation()
|
||||
const simulatorsMetadata = useSimulatorsMetadata()
|
||||
const titre = t('simulateurs.accueil.titre', 'Simulateurs disponibles')
|
||||
return (
|
||||
<>
|
||||
|
@ -27,101 +101,24 @@ export default function Simulateurs() {
|
|||
// dernière ligne.
|
||||
style={{ maxWidth: 1100, margin: 'auto' }}
|
||||
>
|
||||
<Link
|
||||
className="ui__ interactive card box"
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: sitePaths.simulateurs['assimilé-salarié']
|
||||
}}
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji('☂️')}</div>
|
||||
<Trans i18nKey="simulateurs.accueil.assimilé">
|
||||
<h3>Assimilé salarié</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Calculer le revenu d'un dirigeant de SAS, SASU ou SARL
|
||||
minoritaire
|
||||
</p>
|
||||
</Trans>
|
||||
</Link>
|
||||
<Link
|
||||
className="ui__ interactive card box"
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: sitePaths.simulateurs.indépendant
|
||||
}}
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji('👩🔧')}</div>
|
||||
<Trans i18nKey="simulateurs.accueil.indépendant">
|
||||
<h3>Indépendant</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Calculer le revenu d'un dirigeant de EURL, EI, ou SARL
|
||||
majoritaire
|
||||
</p>
|
||||
</Trans>
|
||||
</Link>
|
||||
<Link
|
||||
className="ui__ interactive card box"
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: sitePaths.simulateurs['auto-entrepreneur']
|
||||
}}
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji('🚶♂️')}</div>
|
||||
<Trans i18nKey="simulateurs.accueil.auto">
|
||||
<h3>Auto-entrepreneur</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Calculer le revenu (ou le chiffre d'affaires) d'un
|
||||
auto-entrepreneur
|
||||
</p>
|
||||
</Trans>
|
||||
</Link>
|
||||
<Link
|
||||
className="ui__ interactive card box"
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: sitePaths.simulateurs.salarié
|
||||
}}
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji('🤝')}</div>
|
||||
<Trans i18nKey="simulateurs.accueil.salarié">
|
||||
<h3>Salarié</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Calculer le salaire net, brut, ou total d'un salarié, stagiaire,
|
||||
ou assimilé
|
||||
</p>
|
||||
</Trans>
|
||||
</Link>
|
||||
<Link
|
||||
className="ui__ interactive card box"
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: sitePaths.simulateurs['artiste-auteur']
|
||||
}}
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji('👩🎨')}</div>
|
||||
<Trans i18nKey="simulateurs.accueil.artiste-auteur">
|
||||
<h3>Artiste-auteur</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Estimer les cotisations sociales d'un artiste ou auteur
|
||||
</p>
|
||||
</Trans>
|
||||
</Link>
|
||||
<Link
|
||||
className="ui__ interactive card box"
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: sitePaths.simulateurs.comparaison
|
||||
}}
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji('📊')}</div>
|
||||
<Trans i18nKey="simulateurs.accueil.comparaison">
|
||||
<h3>Comparaison statuts</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
Simulez les différences entre les régimes (cotisations,
|
||||
retraite, maternité, maladie, etc.)
|
||||
</p>
|
||||
</Trans>
|
||||
</Link>
|
||||
{simulatorsMetadata
|
||||
.filter(({ name }) => name !== 'Coronavirus')
|
||||
.map(({ name, description, sitePath, icône }) => (
|
||||
<Link
|
||||
className="ui__ interactive card box"
|
||||
key={sitePath}
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: sitePath
|
||||
}}
|
||||
>
|
||||
<div className="ui__ big box-icon">{emoji(icône)}</div>
|
||||
<h3>{name}</h3>
|
||||
<p className="ui__ notice" css="flex: 1">
|
||||
{description}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import React, { Suspense } from 'react'
|
||||
let Stats = React.lazy(() => import('./Stats'))
|
||||
|
||||
export default function LazyStats() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={<p className="ui__ lead">Chargement de la page stats</p>}
|
||||
>
|
||||
<Stats />
|
||||
</Suspense>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,412 @@
|
|||
import BarChartBranch from 'Components/BarChart'
|
||||
import MoreInfosOnUs from 'Components/MoreInfosOnUs'
|
||||
import { StackedBarChart } from 'Components/StackedBarChart'
|
||||
import { ThemeColorsContext } from 'Components/utils/colors'
|
||||
import { ScrollToTop } from 'Components/utils/Scroll'
|
||||
import { groupWith } from 'ramda'
|
||||
import React, { useContext, useState } from 'react'
|
||||
import emoji from 'react-easy-emoji'
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Line,
|
||||
LineChart,
|
||||
ReferenceArea,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis
|
||||
} from 'recharts'
|
||||
import DefaultTooltipContent from 'recharts/lib/component/DefaultTooltipContent'
|
||||
import styled from 'styled-components'
|
||||
import {
|
||||
formatPercentage,
|
||||
formatValue
|
||||
} from '../../../../../source/engine/format'
|
||||
import statsJson from '../../../../data/stats.json'
|
||||
import { capitalise0 } from '../../../../utils'
|
||||
import Privacy from '../../layout/Footer/Privacy'
|
||||
import { useSimulatorsMetadata } from '../Simulateurs/Home'
|
||||
|
||||
const stats: StatsData = statsJson as any
|
||||
|
||||
const monthPeriods = ['currentMonth', 'oneMonthAgo', 'twoMonthAgo'] as const
|
||||
type MonthPeriod = typeof monthPeriods[number]
|
||||
|
||||
type Periodicity = 'daily' | 'monthly'
|
||||
|
||||
type StatsData = {
|
||||
feedback: {
|
||||
simulator: number
|
||||
content: number
|
||||
}
|
||||
statusChosen: Array<{
|
||||
label: string
|
||||
nb_visits: number
|
||||
}>
|
||||
dailyVisits: Array<{
|
||||
date: string
|
||||
visiteurs: number
|
||||
}>
|
||||
monthlyVisits: Array<{
|
||||
date: string
|
||||
visiteurs: number
|
||||
}>
|
||||
simulators: Record<
|
||||
MonthPeriod,
|
||||
{
|
||||
date: string
|
||||
visites: Array<{ label: string; nb_visits: number }>
|
||||
}
|
||||
>
|
||||
channelType: Record<
|
||||
MonthPeriod,
|
||||
{
|
||||
date: string
|
||||
visites: Array<{ label: string; nb_visits: number }>
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export default function Stats() {
|
||||
const [choice, setChoice] = useState<Periodicity>('monthly')
|
||||
const [choicesimulators, setChoicesimulators] = useState<MonthPeriod>(
|
||||
'oneMonthAgo'
|
||||
)
|
||||
const { palettes } = useContext(ThemeColorsContext)
|
||||
const simulatorsMetadata = useSimulatorsMetadata()
|
||||
return (
|
||||
<>
|
||||
<ScrollToTop />
|
||||
<h1>
|
||||
Statistiques <>{emoji('📊')}</>
|
||||
</h1>
|
||||
<p>
|
||||
Découvrez nos statistiques d'utilisation mises à jour quotidiennement.
|
||||
<br />
|
||||
Les données recueillies sont anonymisées.{' '}
|
||||
<Privacy label="En savoir plus" />
|
||||
</p>
|
||||
<section>
|
||||
<SectionTitle>
|
||||
<h2>Nombre de visites</h2>
|
||||
<span>
|
||||
{emoji('🗓')}{' '}
|
||||
<select
|
||||
onChange={event => {
|
||||
setChoice(event.target.value as Periodicity)
|
||||
}}
|
||||
value={choice}
|
||||
>
|
||||
<option value="monthly">les derniers mois</option>
|
||||
<option value="daily">les derniers jours</option>
|
||||
</select>
|
||||
</span>
|
||||
</SectionTitle>
|
||||
<LineChartVisits periodicity={choice} />
|
||||
|
||||
<Indicators>
|
||||
<Indicator main="1,7 million" subTitle="Visiteurs en 2019" />
|
||||
<Indicator
|
||||
main="52,9%"
|
||||
subTitle="Convertissent en lançant une simulation"
|
||||
/>
|
||||
</Indicators>
|
||||
</section>
|
||||
<section>
|
||||
<SectionTitle>
|
||||
<h2>Nombre d'utilisation des simulateurs</h2>
|
||||
<PeriodSelector
|
||||
onChange={event => {
|
||||
setChoicesimulators(event.target.value as MonthPeriod)
|
||||
}}
|
||||
value={choicesimulators}
|
||||
/>
|
||||
</SectionTitle>
|
||||
|
||||
{stats.simulators[choicesimulators].visites.map(
|
||||
({ label, nb_visits }) => {
|
||||
const details = simulatorsMetadata.find(({ sitePath }) =>
|
||||
sitePath.includes(label)
|
||||
)
|
||||
if (!details) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<BarChartBranch
|
||||
key={label}
|
||||
value={nb_visits}
|
||||
title={
|
||||
<>
|
||||
{details.name}{' '}
|
||||
<Link
|
||||
className="distribution-chart__link_icone"
|
||||
to={{
|
||||
state: { fromSimulateurs: true },
|
||||
pathname: details.sitePath
|
||||
}}
|
||||
title="Accéder au simulateur"
|
||||
css="font-size:0.75em"
|
||||
>
|
||||
{emoji('📎')}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
icon={details.icône}
|
||||
maximum={stats.simulators[choicesimulators].visites.reduce(
|
||||
(a, b) => Math.max(a, b.nb_visits),
|
||||
0
|
||||
)}
|
||||
unit="visiteurs"
|
||||
/>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<SectionTitle>
|
||||
<h2>Origine du trafic</h2>
|
||||
<PeriodSelector
|
||||
onChange={event => {
|
||||
setChoicesimulators(event.target.value as MonthPeriod)
|
||||
}}
|
||||
value={choicesimulators}
|
||||
/>
|
||||
</SectionTitle>
|
||||
|
||||
<StackedBarChart
|
||||
data={stats.channelType[choicesimulators].visites
|
||||
.map((data, i) => ({
|
||||
value: data.nb_visits,
|
||||
key: data.label,
|
||||
legend: capitalise0(data.label),
|
||||
color: palettes[i][0]
|
||||
}))
|
||||
.reverse()}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Avis des visiteurs</h2>
|
||||
<Indicators>
|
||||
<Indicator
|
||||
main={formatPercentage(stats.feedback.simulator)}
|
||||
subTitle="Taux de satisfaction sur les simulateurs"
|
||||
/>
|
||||
<Indicator
|
||||
main={formatPercentage(stats.feedback.content)}
|
||||
subTitle="Taux de satisfaction sur le contenu"
|
||||
/>
|
||||
</Indicators>
|
||||
<p>
|
||||
Ces indicateurs sont calculés à partir des boutons de retours affichés
|
||||
en bas de toutes les pages.
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Statut choisi le mois dernier</h2>
|
||||
{stats.statusChosen.map(x => (
|
||||
<BarChartBranch
|
||||
key={x.label}
|
||||
value={x.nb_visits}
|
||||
title={capitalise0(x.label)}
|
||||
maximum={stats.statusChosen.reduce(
|
||||
(a, b) => Math.max(a, b.nb_visits),
|
||||
0
|
||||
)}
|
||||
unit="visiteurs"
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<MoreInfosOnUs />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const weekEndDays = groupWith(
|
||||
(a, b) => {
|
||||
const dayAfterA = new Date(a)
|
||||
dayAfterA.setDate(dayAfterA.getDate() + 1)
|
||||
return dayAfterA.toISOString().substring(0, 10) === b
|
||||
},
|
||||
stats.dailyVisits
|
||||
.map(({ date }) => new Date(date))
|
||||
.filter(date => date.getDay() === 0 || date.getDay() === 6)
|
||||
.map(date => date.toISOString().substring(0, 10))
|
||||
)
|
||||
|
||||
function PeriodSelector(props: React.ComponentProps<'select'>) {
|
||||
const formatDate = (date: string) =>
|
||||
new Date(date).toLocaleString('default', {
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
})
|
||||
return (
|
||||
<span>
|
||||
{emoji('🗓')}{' '}
|
||||
<select {...props}>
|
||||
{monthPeriods.map(monthPeriod => (
|
||||
<option key={monthPeriod} value={monthPeriod}>
|
||||
{formatDate(stats.simulators[monthPeriod].date)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const Indicators = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
margin: 2rem 0;
|
||||
`
|
||||
|
||||
type IndicatorProps = {
|
||||
main?: string
|
||||
subTitle?: string
|
||||
}
|
||||
|
||||
function Indicator({ main, subTitle }: IndicatorProps) {
|
||||
return (
|
||||
<div
|
||||
css={`
|
||||
text-align: center;
|
||||
width: 210px;
|
||||
`}
|
||||
>
|
||||
<div
|
||||
css={`
|
||||
font-size: 2.3rem;
|
||||
`}
|
||||
>
|
||||
{main}
|
||||
</div>
|
||||
<div>{subTitle}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type LineChartVisitsProps = {
|
||||
periodicity: Periodicity
|
||||
}
|
||||
|
||||
function LineChartVisits({ periodicity }: LineChartVisitsProps) {
|
||||
const { color } = useContext(ThemeColorsContext)
|
||||
const data = periodicity === 'daily' ? stats.dailyVisits : stats.monthlyVisits
|
||||
|
||||
return (
|
||||
<ResponsiveContainer width="100%" height={400}>
|
||||
<LineChart
|
||||
data={data}
|
||||
margin={{
|
||||
top: 5,
|
||||
right: 30,
|
||||
left: 20,
|
||||
bottom: 5
|
||||
}}
|
||||
>
|
||||
<CartesianGrid />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tickFormatter={tickItem => formatDate(tickItem, periodicity)}
|
||||
/>
|
||||
<YAxis
|
||||
dataKey="visiteurs"
|
||||
tickFormatter={tickItem =>
|
||||
formatValue({ value: tickItem, language: 'fr' })
|
||||
}
|
||||
/>
|
||||
{periodicity === 'daily' ? (
|
||||
<Legend
|
||||
payload={[
|
||||
{
|
||||
value: 'Week-End',
|
||||
type: 'rect',
|
||||
color: '#e5e5e5',
|
||||
id: 'weedkend'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
<Tooltip content={<CustomTooltip periodicity={periodicity} />} />
|
||||
{weekEndDays.map(days =>
|
||||
days.length === 2 ? (
|
||||
<ReferenceArea
|
||||
key={days[0]}
|
||||
x1={days[0]}
|
||||
x2={days[1]}
|
||||
strokeOpacity={0.3}
|
||||
/>
|
||||
) : (
|
||||
<ReferenceArea key={days[0]} x1={days[0]} strokeOpacity={0.3} />
|
||||
)
|
||||
)}
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="visiteurs"
|
||||
stroke={color}
|
||||
strokeWidth={3}
|
||||
animationDuration={500}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function formatDate(date: string | Date, periodicity?: Periodicity) {
|
||||
if (periodicity === 'monthly') {
|
||||
return new Date(date).toLocaleString('default', {
|
||||
month: 'short',
|
||||
year: '2-digit'
|
||||
})
|
||||
} else {
|
||||
return new Date(date).toLocaleString('default', {
|
||||
day: '2-digit',
|
||||
month: '2-digit'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type CustomTooltipProps = {
|
||||
active?: boolean
|
||||
periodicity: Periodicity
|
||||
payload?: any
|
||||
}
|
||||
|
||||
const CustomTooltip = ({
|
||||
active,
|
||||
periodicity,
|
||||
payload
|
||||
}: CustomTooltipProps) => {
|
||||
if (!active) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<DefaultTooltipContent
|
||||
payload={[
|
||||
{
|
||||
value: formatDate(payload[0].payload.date, periodicity)
|
||||
},
|
||||
{
|
||||
value: formatValue({
|
||||
value: payload[0].payload.visiteurs,
|
||||
language: 'fr'
|
||||
}),
|
||||
unit: ' visiteurs'
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const SectionTitle = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
`
|
|
@ -127,6 +127,7 @@ export const constructLocalizedSitePath = (language: string) => {
|
|||
},
|
||||
nouveautés: t('path.nouveautés', '/nouveautés'),
|
||||
budget: t('path.budget', '/budget'),
|
||||
stats: t('path.stats', '/stats'),
|
||||
documentation: {
|
||||
index: t('path.documentation.index', '/documentation'),
|
||||
rule: (dottedName: DottedName) => '/' + encodeRuleName(dottedName)
|
||||
|
|
163
yarn.lock
163
yarn.lock
|
@ -1228,6 +1228,18 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
|
||||
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
|
||||
|
||||
"@types/d3-path@*":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-1.0.8.tgz#48e6945a8ff43ee0a1ce85c8cfa2337de85c7c79"
|
||||
integrity sha512-AZGHWslq/oApTAHu9+yH/Bnk63y9oFOMROtqPAtxl5uB6qm1x2lueWdVEjsjjV3Qc2+QfuzKIwIR5MvVBakfzA==
|
||||
|
||||
"@types/d3-shape@*":
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-1.3.2.tgz#a41d9d6b10d02e221696b240caf0b5d0f5a588ec"
|
||||
integrity sha512-LtD8EaNYCaBRzHzaAiIPrfcL3DdIysc81dkGlQvv7WQP3+YXV7b0JJTtR1U3bzeRieS603KF4wUo+ZkJVenh8w==
|
||||
dependencies:
|
||||
"@types/d3-path" "*"
|
||||
|
||||
"@types/history@*":
|
||||
version "4.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
|
||||
|
@ -1411,6 +1423,20 @@
|
|||
"@types/prop-types" "*"
|
||||
csstype "^2.2.0"
|
||||
|
||||
"@types/recharts-scale@*":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/recharts-scale/-/recharts-scale-1.0.0.tgz#348c9220d6d9062c44a9d585d686644a97f7e25d"
|
||||
integrity sha512-HR/PrCcxYb2YHviTqH7CMdL1TUhUZLTUKzfrkMhxm1HTa5mg/QtP8XMiuSPz6dZ6wecazAOu8aYZ5DqkNlgHHQ==
|
||||
|
||||
"@types/recharts@^1.8.9":
|
||||
version "1.8.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.9.tgz#2439f1138253500a1fadc1ae3534ce895a0dd0a3"
|
||||
integrity sha512-J4sZYDfdbFf1aLzenOksdd2s8D/GWh8tftgaE3oMDkvgfOwba0d8DxP0hnE2+WzsSy4fSvHWqrKZv5r2hVH47A==
|
||||
dependencies:
|
||||
"@types/d3-shape" "*"
|
||||
"@types/react" "*"
|
||||
"@types/recharts-scale" "*"
|
||||
|
||||
"@types/sizzle@2.3.2":
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47"
|
||||
|
@ -3469,7 +3495,7 @@ core-js@^0.8.3:
|
|||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-0.8.4.tgz#c22665f1e0d1b9c3c5e1b08dabd1f108695e4fcf"
|
||||
integrity sha1-wiZl8eDRucPF4bCNq9HxCGleT88=
|
||||
|
||||
core-js@^2.4.0:
|
||||
core-js@^2.4.0, core-js@^2.6.10:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
|
||||
integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==
|
||||
|
@ -3771,6 +3797,69 @@ cypress@^3.6.1:
|
|||
url "0.11.0"
|
||||
yauzl "2.10.0"
|
||||
|
||||
d3-array@^1.2.0:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.4.tgz#635ce4d5eea759f6f605863dbcfc30edc737f71f"
|
||||
integrity sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==
|
||||
|
||||
d3-collection@1:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.7.tgz#349bd2aa9977db071091c13144d5e4f16b5b310e"
|
||||
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
|
||||
|
||||
d3-color@1:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.0.tgz#89c45a995ed773b13314f06460df26d60ba0ecaf"
|
||||
integrity sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==
|
||||
|
||||
d3-format@1:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.4.4.tgz#356925f28d0fd7c7983bfad593726fce46844030"
|
||||
integrity sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw==
|
||||
|
||||
d3-interpolate@1, d3-interpolate@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987"
|
||||
integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==
|
||||
dependencies:
|
||||
d3-color "1"
|
||||
|
||||
d3-path@1:
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.9.tgz#48c050bb1fe8c262493a8caf5524e3e9591701cf"
|
||||
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
|
||||
|
||||
d3-scale@^2.1.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.2.2.tgz#4e880e0b2745acaaddd3ede26a9e908a9e17b81f"
|
||||
integrity sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==
|
||||
dependencies:
|
||||
d3-array "^1.2.0"
|
||||
d3-collection "1"
|
||||
d3-format "1"
|
||||
d3-interpolate "1"
|
||||
d3-time "1"
|
||||
d3-time-format "2"
|
||||
|
||||
d3-shape@^1.2.0:
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.3.7.tgz#df63801be07bc986bc54f63789b4fe502992b5d7"
|
||||
integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
|
||||
dependencies:
|
||||
d3-path "1"
|
||||
|
||||
d3-time-format@2:
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.2.3.tgz#0c9a12ee28342b2037e5ea1cf0b9eb4dd75f29cb"
|
||||
integrity sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==
|
||||
dependencies:
|
||||
d3-time "1"
|
||||
|
||||
d3-time@1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1"
|
||||
integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==
|
||||
|
||||
daggy@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/daggy/-/daggy-1.4.0.tgz#178b4722866e1c8fac7b91d4bbf171d3767c1573"
|
||||
|
@ -3838,6 +3927,11 @@ decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
|
||||
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
|
||||
|
||||
decimal.js-light@^2.4.1:
|
||||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.0.tgz#ca7faf504c799326df94b0ab920424fdfc125348"
|
||||
integrity sha512-b3VJCbd2hwUpeRGG3Toob+CRo8W22xplipNhP3tN7TSVB/cyMX71P1vM2Xjc9H74uV6dS2hDDmo/rHq8L87Upg==
|
||||
|
||||
decode-uri-component@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
|
@ -7259,6 +7353,11 @@ lodash.camelcase@^4.3.0:
|
|||
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
|
||||
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
||||
|
||||
lodash.escape@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-4.0.1.tgz#c9044690c21e04294beaa517712fded1fa88de98"
|
||||
|
@ -7324,12 +7423,17 @@ lodash.templatesettings@^4.0.0:
|
|||
dependencies:
|
||||
lodash._reinterpolate "^3.0.0"
|
||||
|
||||
lodash.throttle@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4"
|
||||
integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ=
|
||||
|
||||
lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash@4.17.15, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0:
|
||||
lodash@4.17.15, lodash@^4.0.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@~4.17.4:
|
||||
version "4.17.15"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||
|
@ -9229,7 +9333,7 @@ quick-temp@^0.1.3:
|
|||
rimraf "^2.5.4"
|
||||
underscore.string "~3.3.4"
|
||||
|
||||
raf@^3.4.1:
|
||||
raf@^3.4.0, raf@^3.4.1:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.1.tgz#0742e99a4a6552f445d73e3ee0328af0ff1ede39"
|
||||
integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==
|
||||
|
@ -9477,6 +9581,16 @@ react-redux@^7.0.3:
|
|||
prop-types "^15.7.2"
|
||||
react-is "^16.9.0"
|
||||
|
||||
react-resize-detector@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c"
|
||||
integrity sha512-oCAddEWWeFWYH5FAcHdBYcZjAw9fMzRUK9sWSx6WvSSOPVRxcHd5zTIGy/mOus+AhN/u6T4TMiWxvq79PywnJQ==
|
||||
dependencies:
|
||||
lodash.debounce "^4.0.8"
|
||||
lodash.throttle "^4.1.1"
|
||||
prop-types "^15.6.0"
|
||||
resize-observer-polyfill "^1.5.0"
|
||||
|
||||
react-router-dom@^5.1.1:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
|
||||
|
@ -9520,6 +9634,16 @@ react-side-effect@^1.1.0:
|
|||
dependencies:
|
||||
shallowequal "^1.0.1"
|
||||
|
||||
react-smooth@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-smooth/-/react-smooth-1.0.5.tgz#94ae161d7951cdd893ccb7099d031d342cb762ad"
|
||||
integrity sha512-eW057HT0lFgCKh8ilr0y2JaH2YbNcuEdFpxyg7Gf/qDKk9hqGMyXryZJ8iMGJEuKH0+wxS0ccSsBBB3W8yCn8w==
|
||||
dependencies:
|
||||
lodash "~4.17.4"
|
||||
prop-types "^15.6.0"
|
||||
raf "^3.4.0"
|
||||
react-transition-group "^2.5.0"
|
||||
|
||||
react-spring@=8.0.27:
|
||||
version "8.0.27"
|
||||
resolved "https://registry.yarnpkg.com/react-spring/-/react-spring-8.0.27.tgz#97d4dee677f41e0b2adcb696f3839680a3aa356a"
|
||||
|
@ -9567,7 +9691,7 @@ react-transition-group@^1.2.0:
|
|||
prop-types "^15.5.6"
|
||||
warning "^3.0.0"
|
||||
|
||||
react-transition-group@^2.2.1:
|
||||
react-transition-group@^2.2.1, react-transition-group@^2.5.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-2.9.0.tgz#df9cdb025796211151a436c69a8f3b97b5b07c8d"
|
||||
integrity sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==
|
||||
|
@ -9658,7 +9782,31 @@ recast@~0.11.12:
|
|||
private "~0.1.5"
|
||||
source-map "~0.5.0"
|
||||
|
||||
reduce-css-calc@^1.2.6:
|
||||
recharts-scale@^0.4.2:
|
||||
version "0.4.3"
|
||||
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.3.tgz#040b4f638ed687a530357292ecac880578384b59"
|
||||
integrity sha512-t8p5sccG9Blm7c1JQK/ak9O8o95WGhNXD7TXg/BW5bYbVlr6eCeRBNpgyigD4p6pSSMehC5nSvBUPj6F68rbFA==
|
||||
dependencies:
|
||||
decimal.js-light "^2.4.1"
|
||||
|
||||
recharts@^1.8.5:
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/recharts/-/recharts-1.8.5.tgz#ca94a3395550946334a802e35004ceb2583fdb12"
|
||||
integrity sha512-tM9mprJbXVEBxjM7zHsIy6Cc41oO/pVYqyAsOHLxlJrbNBuLs0PHB3iys2M+RqCF0//k8nJtZF6X6swSkWY3tg==
|
||||
dependencies:
|
||||
classnames "^2.2.5"
|
||||
core-js "^2.6.10"
|
||||
d3-interpolate "^1.3.0"
|
||||
d3-scale "^2.1.0"
|
||||
d3-shape "^1.2.0"
|
||||
lodash "^4.17.5"
|
||||
prop-types "^15.6.0"
|
||||
react-resize-detector "^2.3.0"
|
||||
react-smooth "^1.0.5"
|
||||
recharts-scale "^0.4.2"
|
||||
reduce-css-calc "^1.3.0"
|
||||
|
||||
reduce-css-calc@^1.2.6, reduce-css-calc@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/reduce-css-calc/-/reduce-css-calc-1.3.0.tgz#747c914e049614a4c9cfbba629871ad1d2927716"
|
||||
integrity sha1-dHyRTgSWFKTJz7umKYca0dKSdxY=
|
||||
|
@ -9977,6 +10125,11 @@ reselect@^4.0.0:
|
|||
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
|
||||
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
|
||||
|
||||
resize-observer-polyfill@^1.5.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-cwd@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
|
||||
|
|
Loading…
Reference in New Issue