Initial commit
This commit is contained in:
commit
80c1729b2f
26 changed files with 311019 additions and 0 deletions
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
build/
|
||||||
|
node_modules/
|
||||||
|
serviceWorker.js
|
16
.eslintrc.js
Normal file
16
.eslintrc.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module.exports = {
|
||||||
|
parser: 'babel-eslint',
|
||||||
|
extends: ['airbnb', 'prettier', 'plugin:flowtype/recommended'],
|
||||||
|
plugins: ['react', 'jsx-a11y', 'import', 'flowtype'],
|
||||||
|
rules: {
|
||||||
|
'react/jsx-filename-extension': [1, { extensions: ['.js', '.jsx'] }],
|
||||||
|
'no-underscore-dangle': ["error", { "allow": ["_ne", "_sw"] }],
|
||||||
|
"react/jsx-props-no-spreading": [1, {
|
||||||
|
"exceptions": ["ReactMapGL"]
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es6: true,
|
||||||
|
}
|
||||||
|
};
|
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
Ce projet a simplement pour but de répertorier l'ensemble des stations essences en France.
|
||||||
|
|
||||||
|
Les données sont basées sur les [Open Data](https://www.prix-carburants.gouv.fr/rubrique/opendata/).
|
64
package.json
Normal file
64
package.json
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
{
|
||||||
|
"name": "e85map",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@testing-library/jest-dom": "^4.2.4",
|
||||||
|
"@testing-library/react": "^9.3.2",
|
||||||
|
"@testing-library/user-event": "^7.1.2",
|
||||||
|
"bootstrap": "^4.4.1",
|
||||||
|
"eslint-config-prettier": "^6.10.0",
|
||||||
|
"eslint-plugin-prettier": "^3.1.2",
|
||||||
|
"mapbox-gl": "^1.8.1",
|
||||||
|
"moment": "^2.24.0",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react": "^16.13.0",
|
||||||
|
"react-bootstrap": "^1.0.0-beta.17",
|
||||||
|
"react-dom": "^16.13.0",
|
||||||
|
"react-map-gl": "^5.2.3",
|
||||||
|
"react-moment": "^0.9.7",
|
||||||
|
"react-scripts": "3.4.0",
|
||||||
|
"react-toast-notifications": "^2.4.0",
|
||||||
|
"xml-reader": "^2.4.3"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "react-scripts start",
|
||||||
|
"build": "react-scripts build && ssh porto 'rm -r www/darkou.fr/carburants/static' && scp -r build/* porto:www/darkou.fr/carburants",
|
||||||
|
"test": "react-scripts test",
|
||||||
|
"eject": "react-scripts eject"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"extends": "react-app"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
">0.2%",
|
||||||
|
"not dead",
|
||||||
|
"not op_mini all"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 chrome version",
|
||||||
|
"last 1 firefox version",
|
||||||
|
"last 1 safari version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"husky": {
|
||||||
|
"hooks": {
|
||||||
|
"pre-commit": "lint-staged"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*.js": [
|
||||||
|
"./node_modules/.bin/eslint --fix",
|
||||||
|
"git add"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": "^6.8.0",
|
||||||
|
"eslint-config-airbnb": "^18.0.1",
|
||||||
|
"eslint-plugin-import": "^2.20.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.2.3",
|
||||||
|
"eslint-plugin-react": "^7.18.3",
|
||||||
|
"eslint-plugin-react-hooks": "^1.7.0"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/car.png
Normal file
BIN
public/car.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
BIN
public/gas-station.png
Normal file
BIN
public/gas-station.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 991 B |
298705
public/gasStations.xml
Normal file
298705
public/gasStations.xml
Normal file
File diff suppressed because it is too large
Load diff
21
public/index.html
Normal file
21
public/index.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
<meta name="description" content="Web site created using create-react-app" />
|
||||||
|
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||||
|
|
||||||
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
<title>Carte des stations e85 - un service proposé par DarKou.fr</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
BIN
public/logo192.png
Normal file
BIN
public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"short_name": "Prix Carburant",
|
||||||
|
"name": "Prix des carburant - un service DarKou.fr",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "favicon.ico",
|
||||||
|
"sizes": "64x64 32x32 24x24 16x16",
|
||||||
|
"type": "image/x-icon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo192.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "logo512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff"
|
||||||
|
}
|
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
BIN
public/waze.png
Normal file
BIN
public/waze.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
24
src/App.css
Normal file
24
src/App.css
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
.mapContainer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebarStyle {
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin: 12px;
|
||||||
|
background-color: #404040;
|
||||||
|
color: #ffffff;
|
||||||
|
z-index: 1 !important;
|
||||||
|
padding: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locationIcon {
|
||||||
|
width: 32px;
|
||||||
|
}
|
90
src/App.js
Normal file
90
src/App.js
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ToastProvider } from 'react-toast-notifications';
|
||||||
|
import Footer from './Components/Footer';
|
||||||
|
import GasStation from "./Components/GasStation";
|
||||||
|
import Map from "./Components/Map";
|
||||||
|
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
|
||||||
|
import './App.css';
|
||||||
|
|
||||||
|
class Application extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showModal: false,
|
||||||
|
selectedGasStation: {},
|
||||||
|
selectedGasType: 'E85'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode permettant de sélectionner le type de carburant à afficher
|
||||||
|
* @param {Object} e
|
||||||
|
*/
|
||||||
|
selectGasType = (e) => {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
} = e.target;
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
selectedGasType: value
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode permettant de fermer la modale contenant les détails de la station
|
||||||
|
* @param {Boolean} goToWaze
|
||||||
|
*/
|
||||||
|
hideModal = (goToWaze) => {
|
||||||
|
const {
|
||||||
|
selectedGasStation,
|
||||||
|
} = this.state;
|
||||||
|
if (goToWaze) {
|
||||||
|
window.open(`https://www.waze.com/livemap/directions?navigate=yes&latlng=${selectedGasStation.latitude}%2C${selectedGasStation.longitude}&zoom=17`);
|
||||||
|
}
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
selectedGasStation: {},
|
||||||
|
showModal: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode permettant de sélectionner et d'afficher le détails d'une station
|
||||||
|
* @param {Object} selectedGasStation
|
||||||
|
*/
|
||||||
|
showGasStation = (selectedGasStation) => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
showModal: true,
|
||||||
|
selectedGasStation
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode gérant le rendu de la vue
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
selectedGasType,
|
||||||
|
showModal,
|
||||||
|
selectedGasStation,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider>
|
||||||
|
<GasStation
|
||||||
|
showModal={showModal}
|
||||||
|
hideModal={this.hideModal}
|
||||||
|
selectedGasStation={selectedGasStation}
|
||||||
|
/>
|
||||||
|
<Map selectedGasType={selectedGasType} showGasStation={this.showGasStation} />
|
||||||
|
<Footer selectedGasType={selectedGasType} selectGasType={this.selectGasType} />
|
||||||
|
</ToastProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Application;
|
25
src/Components/Footer.js
Normal file
25
src/Components/Footer.js
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Navbar } from "react-bootstrap";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import GasTypes from "./GasTypes";
|
||||||
|
|
||||||
|
const Footer = (props) => {
|
||||||
|
const {
|
||||||
|
selectGasType,
|
||||||
|
selectedGasType,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar bg="light" variant="light" fixed="bottom">
|
||||||
|
<GasTypes selectedGasType={selectedGasType} selectGasType={selectGasType} />
|
||||||
|
</Navbar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Footer.propTypes = {
|
||||||
|
selectedGasType: PropTypes.string.isRequired,
|
||||||
|
selectGasType: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
79
src/Components/GasStation.js
Normal file
79
src/Components/GasStation.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal, Button, ListGroup } from "react-bootstrap";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
class GasStation extends React.Component {
|
||||||
|
renderPrices = () => {
|
||||||
|
const {
|
||||||
|
selectedGasStation,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListGroup variant="flush">
|
||||||
|
{selectedGasStation.prices ? selectedGasStation.prices.map(price => {
|
||||||
|
return (
|
||||||
|
<ListGroup.Item key={price.type}>
|
||||||
|
{`${price.type} : ${price.price} € `}
|
||||||
|
</ListGroup.Item>
|
||||||
|
);
|
||||||
|
}) : (null)}
|
||||||
|
</ListGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const {
|
||||||
|
showModal,
|
||||||
|
hideModal,
|
||||||
|
selectedGasStation,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
size="xl"
|
||||||
|
aria-labelledby="contained-modal-title-vcenter"
|
||||||
|
centered
|
||||||
|
show={showModal}
|
||||||
|
onHide={hideModal}
|
||||||
|
>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>
|
||||||
|
{`${selectedGasStation.address} - ${selectedGasStation.city}`}
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
{this.renderPrices()}
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="primary" onClick={() => hideModal(true)}>
|
||||||
|
<img
|
||||||
|
className="locationIcon"
|
||||||
|
src="/waze.png"
|
||||||
|
alt="S'y rendre"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GasStation.defaultProps = {
|
||||||
|
selectedGasStation: {
|
||||||
|
address: null,
|
||||||
|
city: null,
|
||||||
|
prices: []
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
GasStation.propTypes = {
|
||||||
|
showModal: PropTypes.bool.isRequired,
|
||||||
|
hideModal: PropTypes.func.isRequired,
|
||||||
|
selectedGasStation: PropTypes.shape({
|
||||||
|
address: PropTypes.string,
|
||||||
|
city: PropTypes.string,
|
||||||
|
prices: PropTypes.array
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GasStation;
|
64
src/Components/GasTypes.js
Normal file
64
src/Components/GasTypes.js
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Form, Row, Col } from "react-bootstrap";
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
class GasTypes extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
gasTypes: [
|
||||||
|
{
|
||||||
|
name: "Ethanol e85",
|
||||||
|
type: 'E85',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sans plomb 95 E10",
|
||||||
|
type: "E10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sans plomb 95",
|
||||||
|
type: "SP95"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sans plomb 98",
|
||||||
|
type: "SP98"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Gazole",
|
||||||
|
type: "Gazole"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "GPL",
|
||||||
|
type: "GPLc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
gasTypes,
|
||||||
|
} = this.state;
|
||||||
|
const {
|
||||||
|
selectGasType,
|
||||||
|
selectedGasType,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row style={{ width: "100%" }}>
|
||||||
|
<Col>
|
||||||
|
<Form.Control as="select" value={selectedGasType} onChange={selectGasType}>
|
||||||
|
{gasTypes.map(gasType => (<option key={gasType.type} value={gasType.type}>{gasType.name}</option>))}
|
||||||
|
</Form.Control>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GasTypes.propTypes = {
|
||||||
|
selectedGasType: PropTypes.string.isRequired,
|
||||||
|
selectGasType: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GasTypes;
|
187
src/Components/Map.js
Normal file
187
src/Components/Map.js
Normal file
|
@ -0,0 +1,187 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactMapGL, { Marker } from 'react-map-gl';
|
||||||
|
import { Button } from "react-bootstrap";
|
||||||
|
import XmlReader from 'xml-reader';
|
||||||
|
import { withToastManager } from 'react-toast-notifications';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { haveSelectedGas, extractGasStationFromXml } from '../helpers';
|
||||||
|
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||||
|
|
||||||
|
const mapboxToken = 'pk.eyJ1IjoiZGFya291IiwiYSI6ImNrNzkwdmlsdTBtMmwzZnM0ZmI4Z3h4czIifQ.GU2CdcMiKiApHNhI0ylGtQ';
|
||||||
|
|
||||||
|
class Map extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
viewport: {
|
||||||
|
width: '100vw',
|
||||||
|
height: '100vh',
|
||||||
|
latitude: 44.837789,
|
||||||
|
longitude: -0.57918,
|
||||||
|
zoom: 11,
|
||||||
|
},
|
||||||
|
userLocation: {
|
||||||
|
// latitude: 44.837789,
|
||||||
|
// longitude: -0.57918,
|
||||||
|
},
|
||||||
|
gasStations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.mapRef = React.createRef();
|
||||||
|
|
||||||
|
this.loadGasStations();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.setUserLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode permettant de mettre à jour la position de la map
|
||||||
|
* @param {Object} viewport
|
||||||
|
*/
|
||||||
|
onViewportChange = (viewport) => {
|
||||||
|
this.setState({ viewport });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode permettant de sauvegarder la position de l'utilisateur
|
||||||
|
*/
|
||||||
|
setUserLocation() {
|
||||||
|
navigator.geolocation.getCurrentPosition((position) => {
|
||||||
|
const setUserLocation = {
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude,
|
||||||
|
};
|
||||||
|
const newViewport = {
|
||||||
|
height: '100vh',
|
||||||
|
width: '100vw',
|
||||||
|
latitude: position.coords.latitude,
|
||||||
|
longitude: position.coords.longitude,
|
||||||
|
zoom: 10,
|
||||||
|
};
|
||||||
|
this.setState({
|
||||||
|
viewport: newViewport,
|
||||||
|
userLocation: setUserLocation,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode permettant de filter sur la liste des stations affichables sur la carte
|
||||||
|
* @param {Object} gasStation
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
displayThisGasStation = (gasStation) => {
|
||||||
|
const mapGL = this.mapRef.getMap();
|
||||||
|
const bounds = mapGL.getBounds();
|
||||||
|
|
||||||
|
return (bounds._ne.lat > gasStation.latitude && bounds._sw.lat < gasStation.latitude)
|
||||||
|
&& (bounds._ne.lng > gasStation.longitude && bounds._sw.lng < gasStation.longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode permettant de charger le fichier xml contenant la liste des stations
|
||||||
|
*/
|
||||||
|
loadGasStations() {
|
||||||
|
const {
|
||||||
|
toastManager,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
fetch('/gasStations.xml')
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((response) => {
|
||||||
|
const reader = XmlReader.create();
|
||||||
|
|
||||||
|
reader.on('done', (data) => {
|
||||||
|
const pdv = data.children;
|
||||||
|
|
||||||
|
const gasStations = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < pdv.length; i += 1) {
|
||||||
|
const currentPdv = pdv[i];
|
||||||
|
gasStations.push(extractGasStationFromXml(currentPdv));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
gasStations,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
reader.parse(response);
|
||||||
|
}).catch(() => {
|
||||||
|
toastManager.add('Erreur lors du chargement de la liste des stations', { appearance: 'error', autoDismiss: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Méthode gérant le rendu de la vue
|
||||||
|
*/
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
viewport,
|
||||||
|
userLocation,
|
||||||
|
gasStations,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
showGasStation,
|
||||||
|
selectedGasType,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ReactMapGL
|
||||||
|
{...viewport}
|
||||||
|
mapStyle="mapbox://styles/mapbox/outdoors-v11"
|
||||||
|
onViewportChange={this.onViewportChange}
|
||||||
|
mapboxApiAccessToken={mapboxToken}
|
||||||
|
ref={(map) => { if (map) this.mapRef = map; }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
Object.keys(userLocation).length !== 0 ? (
|
||||||
|
<Marker
|
||||||
|
latitude={userLocation.latitude}
|
||||||
|
longitude={userLocation.longitude}
|
||||||
|
>
|
||||||
|
<img className="locationIcon" src="/car.png" alt="My position" />
|
||||||
|
</Marker>
|
||||||
|
) : (null)
|
||||||
|
}
|
||||||
|
{gasStations.filter(this.displayThisGasStation).filter((station) => haveSelectedGas(station, selectedGasType)).map((gasStation) => (
|
||||||
|
<Marker
|
||||||
|
key={gasStation.id}
|
||||||
|
latitude={gasStation.latitude}
|
||||||
|
longitude={gasStation.longitude}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => showGasStation(gasStation)}
|
||||||
|
onFocus={() => { }}
|
||||||
|
onBlur={() => { }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="locationIcon"
|
||||||
|
src="/gas-station.png"
|
||||||
|
alt={`${gasStation.id}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
</Marker>
|
||||||
|
))}
|
||||||
|
</ReactMapGL>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.propTypes = {
|
||||||
|
selectedGasType: PropTypes.string.isRequired,
|
||||||
|
showGasStation: PropTypes.func.isRequired,
|
||||||
|
toastManager: PropTypes.shape({
|
||||||
|
add: PropTypes.func,
|
||||||
|
}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withToastManager(Map);
|
63
src/helpers.js
Normal file
63
src/helpers.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
export const getFuelPrices = (pdv) => {
|
||||||
|
const prices = []
|
||||||
|
if (!pdv.children || pdv.children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < pdv.children.length; i += 1) {
|
||||||
|
const currentChildren = pdv.children[i];
|
||||||
|
if (currentChildren.type === 'element' && currentChildren.name === 'prix') {
|
||||||
|
prices.push({
|
||||||
|
type: currentChildren.attributes.nom,
|
||||||
|
price: parseInt(currentChildren.attributes.valeur, 10) / 1000,
|
||||||
|
updatedAt: currentChildren.attributes.maj,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prices;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPlvInformation = (pdv, name) => {
|
||||||
|
if (!pdv.children || pdv.children.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < pdv.children.length; i += 1) {
|
||||||
|
const currentChildren = pdv.children[i];
|
||||||
|
if (currentChildren.type === 'element' && currentChildren.name === name) {
|
||||||
|
|
||||||
|
if ( currentChildren.children && currentChildren.children.length > 0 ) {
|
||||||
|
return currentChildren.children[0].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const formatPosition = (value) => value / 100000;
|
||||||
|
|
||||||
|
export const haveSelectedGas = (station, gas) => {
|
||||||
|
if (!station.prices || station.prices.length === 0 ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0 ; i < station.prices.length ; i +=1 ){
|
||||||
|
if (station.prices[i].type === gas ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractGasStationFromXml = (currentPdv ) => {
|
||||||
|
return {
|
||||||
|
id: currentPdv.attributes.id,
|
||||||
|
latitude: formatPosition(currentPdv.attributes.latitude),
|
||||||
|
longitude: formatPosition(currentPdv.attributes.longitude),
|
||||||
|
prices: getFuelPrices(currentPdv),
|
||||||
|
postCode: currentPdv.attributes.cp,
|
||||||
|
address: getPlvInformation(currentPdv, 'adresse'),
|
||||||
|
city: getPlvInformation(currentPdv, 'ville')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
src/index.js
Normal file
12
src/index.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import App from './App';
|
||||||
|
import * as serviceWorker from './serviceWorker';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
|
ReactDOM.render(<App />, document.getElementById('root'));
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||||
|
serviceWorker.register();
|
129
src/serviceWorker.js
Normal file
129
src/serviceWorker.js
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
const isLocalhost = Boolean(
|
||||||
|
window.location.hostname === 'localhost' ||
|
||||||
|
// [::1] is the IPv6 localhost address.
|
||||||
|
window.location.hostname === '[::1]' ||
|
||||||
|
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||||
|
window.location.hostname.match(
|
||||||
|
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
export function register(config) {
|
||||||
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
|
// The URL constructor is available in all browsers that support SW.
|
||||||
|
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||||
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
|
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||||
|
// from what our page is served on. This might happen if a CDN is used to
|
||||||
|
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||||
|
|
||||||
|
if (isLocalhost) {
|
||||||
|
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||||
|
checkValidServiceWorker(swUrl, config);
|
||||||
|
|
||||||
|
// Add some additional logging to localhost, pointing developers to the
|
||||||
|
// service worker/PWA documentation.
|
||||||
|
navigator.serviceWorker.ready.then(() => {
|
||||||
|
console.log(
|
||||||
|
'This web app is being served cache-first by a service ' +
|
||||||
|
'worker. To learn more, visit https://bit.ly/CRA-PWA'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Is not localhost. Just register service worker
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerValidSW(swUrl, config) {
|
||||||
|
navigator.serviceWorker
|
||||||
|
.register(swUrl)
|
||||||
|
.then(registration => {
|
||||||
|
registration.onupdatefound = () => {
|
||||||
|
const installingWorker = registration.installing;
|
||||||
|
if (installingWorker == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
installingWorker.onstatechange = () => {
|
||||||
|
if (installingWorker.state === 'installed') {
|
||||||
|
if (navigator.serviceWorker.controller) {
|
||||||
|
// At this point, the updated precached content has been fetched,
|
||||||
|
// but the previous service worker will still serve the older
|
||||||
|
// content until all client tabs are closed.
|
||||||
|
console.log(
|
||||||
|
'New content is available and will be used when all ' +
|
||||||
|
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onUpdate) {
|
||||||
|
config.onUpdate(registration);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// At this point, everything has been precached.
|
||||||
|
// It's the perfect time to display a
|
||||||
|
// "Content is cached for offline use." message.
|
||||||
|
console.log('Content is cached for offline use.');
|
||||||
|
|
||||||
|
// Execute callback
|
||||||
|
if (config && config.onSuccess) {
|
||||||
|
config.onSuccess(registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during service worker registration:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkValidServiceWorker(swUrl, config) {
|
||||||
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
|
fetch(swUrl, {
|
||||||
|
headers: { 'Service-Worker': 'script' }
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (
|
||||||
|
response.status === 404 ||
|
||||||
|
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||||
|
) {
|
||||||
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
|
navigator.serviceWorker.ready.then(registration => {
|
||||||
|
registration.unregister().then(() => {
|
||||||
|
window.location.reload();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Service worker found. Proceed as normal.
|
||||||
|
registerValidSW(swUrl, config);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.log(
|
||||||
|
'No internet connection found. App is running in offline mode.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unregister() {
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
navigator.serviceWorker.ready
|
||||||
|
.then(registration => {
|
||||||
|
registration.unregister();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
5
src/setupTests.js
Normal file
5
src/setupTests.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||||
|
// allows you to do things like:
|
||||||
|
// expect(element).toHaveTextContent(/react/i)
|
||||||
|
// learn more: https://github.com/testing-library/jest-dom
|
||||||
|
import '@testing-library/jest-dom/extend-expect';
|
Loading…
Reference in a new issue