Drop trainvel backend, this is now a frontend-only app

This commit is contained in:
2024-11-10 16:34:18 +01:00
parent bc23d63c43
commit 036e1604bd
84 changed files with 22 additions and 7984 deletions

19
src/App.css Normal file
View File

@ -0,0 +1,19 @@
.shrink-enter {
transform: translateY(100%);
opacity: 0;
}
.shrink-enter-active {
transform: translateY(0);
opacity: 1;
transition: transform 500ms ease-out, opacity 500ms ease-out;
}
.shrink-exit {
transform: translateY(0);
height: 100%;
opacity: 1;
}
.shrink-exit-active {
transform: translateY(-100%);
opacity: 0;
transition: transform 500ms ease-in, opacity 500ms ease-in;
}

78
src/App.js Normal file
View File

@ -0,0 +1,78 @@
import {createBrowserRouter, RouterProvider} from "react-router-dom"
import Station from "./Station"
import {createTheme, CssBaseline, ThemeProvider, useMediaQuery} from "@mui/material"
import React, {useMemo} from "react"
import {frFR, LocalizationProvider} from "@mui/x-date-pickers"
import {AdapterDayjs} from "@mui/x-date-pickers/AdapterDayjs"
import 'dayjs/locale/fr'
import './App.css'
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {createSyncStoragePersister} from "@tanstack/query-sync-storage-persister";
import {persistQueryClient} from "@tanstack/react-query-persist-client";
import Home from "./Home";
function App() {
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/station/:theme/:stationSlug",
element: <Station />
}
])
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const theme = useMemo(
() =>
createTheme({
palette: {
mode: prefersDarkMode ? 'dark' : 'light',
sncf: {
departures: {
dark: "#003A79",
light: "#0064AB",
},
arrivals: {
dark: "#1F5628",
light: "#187936",
},
}
},
}),
[prefersDarkMode],
);
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24, // 3 hours
staleTime: 1000 * 60 * 60 * 3, // 3 hours
notifyOnChangeProps: ['data', 'error'],
},
},
})
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
})
persistQueryClient({
queryClient,
persister: localStoragePersister,
})
return <>
<ThemeProvider theme={theme}>
<CssBaseline />
<LocalizationProvider dateAdapter={AdapterDayjs} localeText={frFR.components.MuiLocalizationProvider.defaultProps.localeText} adapterLocale="fr">
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</LocalizationProvider>
</ThemeProvider>
</>
}
export default App;

View File

@ -0,0 +1,46 @@
import {Autocomplete, TextField} from "@mui/material";
import {useRef, useState} from "react";
function AutocompleteStation(params) {
const [options, setOptions] = useState([])
const previousController = useRef()
function onInputChange(event, value) {
if (!value) {
setOptions([])
return
}
if (previousController.current)
previousController.current.abort()
const controller = new AbortController()
const signal = controller.signal
previousController.current = controller
fetch("/api/core/station/?search=" + value, {signal})
.then(response => response.json())
.then(data => data.results)
.then(setOptions)
.catch()
}
return <>
<Autocomplete
id="stop"
options={options}
onInputChange={onInputChange}
filterOptions={(x) => x}
getOptionKey={option => option.id}
getOptionLabel={option => option.name}
groupBy={option => getOptionGroup(option)}
isOptionEqualToValue={(option, value) => option.id === value.id}
renderInput={(params) => <TextField {...params} label="Arrêt" />}
{...params} />
</>
}
function getOptionGroup(option) {
return option.country
}
export default AutocompleteStation;

20
src/Home.js Normal file
View File

@ -0,0 +1,20 @@
import AutocompleteStation from "./AutocompleteStation"
import {useNavigate} from "react-router-dom"
function Home() {
const navigate = useNavigate()
function onStationSelected(event, station) {
navigate(`/station/sncf/${station.slug}/`)
}
return <>
<h1>Horaires des trains</h1>
<h2>
Choisissez une gare dont vous désirez connaître le tableau des prochains départs et arrivées :
</h2>
<AutocompleteStation onChange={onStationSelected} />
</>
}
export default Home;

79
src/Station.js Normal file
View File

@ -0,0 +1,79 @@
import {useNavigate, useParams, useSearchParams} from "react-router-dom"
import TrainsTable from "./TrainsTable"
import TripsFilter from "./TripsFilter"
import {useState} from "react";
import {Box, Button, FormLabel} from "@mui/material";
import {DatePicker, TimePicker} from "@mui/x-date-pickers";
import dayjs from "dayjs";
import {useQuery, useQueryClient} from "@tanstack/react-query";
import AutocompleteStation from "./AutocompleteStation";
function DateTimeSelector({station, date, time}) {
const navigate = useNavigate()
function onStationSelected(event, station) {
if (station !== null)
navigate(`/station/sncf/${station.slug}/`)
}
return <>
<Box component="form" display="flex" alignItems="center" sx={{'& .MuiTextField-root': { m: 1, width: '25ch' },}}>
<FormLabel>
Changer la gare recherchée :
</FormLabel>
<AutocompleteStation onChange={onStationSelected} />
<FormLabel>
Modifier la date et l'heure de recherche :
</FormLabel>
<DatePicker name="date" label="Date" format="YYYY-MM-DD" defaultValue={dayjs(`${date}`)} />
<TimePicker name="time" label="Heure" format="HH:mm" defaultValue={dayjs(`${date} ${time}`)} />
<Button type="submit">Rechercher</Button>
</Box>
</>
}
function Station() {
let {theme, stationSlug} = useParams()
let [searchParams, _setSearchParams] = useSearchParams()
const now = new Date()
let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
let timeNow = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
let [date, setDate] = useState(searchParams.get('date') || dateNow)
let [time, setTime] = useState(searchParams.get('time') || timeNow)
useQueryClient()
const stationQuery = useQuery({
queryKey: ['station', stationSlug],
queryFn: () => fetch(`/api/core/station/${stationSlug}/`)
.then(response => response.json()),
enabled: !!stationSlug,
})
const station = stationQuery.data ?? {name: "Chargement…"}
if (time === timeNow) {
setInterval(() => {
const now = new Date()
let dateNow = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`
let timeNow = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
setDate(dateNow)
setTime(timeNow)
}, 5000)
}
return (
<div className="Station">
<header className="App-header">
<h1>Horaires en gare de {station.name}</h1>
</header>
<main>
<DateTimeSelector station={station} date={date} time={time} />
<TripsFilter />
<TrainsTable station={station} date={date} time={time} tableType="departures" />
<TrainsTable station={station} date={date} time={time} tableType="arrivals" />
</main>
</div>
)
}
export default Station;

8
src/Station.test.js Normal file
View File

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import Station from './Station';
test('renders learn react link', () => {
render(<Station />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

364
src/TrainsTable.js Normal file
View File

@ -0,0 +1,364 @@
import {
Box,
styled,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from "@mui/material"
import {CSSTransition, TransitionGroup} from 'react-transition-group'
import {useQueries, useQuery} from "@tanstack/react-query";
import {useCallback, useEffect, useMemo, useRef, useState} from "react";
const StyledTableRow = styled(TableRow)(({ theme, tabletype }) => ({
'tbody &:nth-of-type(odd)': {
backgroundColor: theme.palette.sncf[tabletype].light,
},
'th, &:nth-of-type(even)': {
backgroundColor: theme.palette.sncf[tabletype].dark,
},
// hide last border
'&:last-child td, &:last-child th': {
border: 0,
},
}));
function TrainsTable({station, date, time, tableType}) {
return <>
<TableContainer>
<Table>
<TrainsTableHeader tableType={tableType} />
<TrainsTableBody station={station} date={date} time={time} tableType={tableType} />
</Table>
</TableContainer>
</>
}
function TrainsTableHeader({tableType}) {
return <>
<TableHead>
<StyledTableRow tabletype={tableType}>
<TableCell colSpan="2" fontSize={16} fontWeight="bold">Train</TableCell>
<TableCell fontSize={16} fontWeight="bold">Heure</TableCell>
<TableCell fontSize={16} fontWeight="bold">Destination</TableCell>
</StyledTableRow>
</TableHead>
</>
}
function TrainsTableBody({station, date, time, tableType}) {
const filterTime = useCallback((train) => {
if (tableType === "departures")
return `${train.departure_date}T${train.departure_time_24h}` >= `${date}T${time}`
else
return `${train.arrival_date}T${train.arrival_time_24h}` >= `${date}T${time}`
}, [date, time, tableType])
const updateTrains = useCallback(() => {
return fetch(`/api/station/next_${tableType}/?station_slug=${station.slug}&date=${date}&time=${time}&offset=${0}&limit=${20}`)
.then(response => response.json())
.then(data => data.results)
.then(data => [...data])
}, [station.id, date, time, tableType])
const trainsQuery = useQuery({
queryKey: ['trains', station.id, tableType],
queryFn: updateTrains,
enabled: !!station.id,
})
const trains = useMemo(() => trainsQuery.data ?? [], [trainsQuery.data])
useEffect(() => {
let validTrains = trains?.filter(filterTime) ?? []
if (trains?.length > 0 && validTrains.length < trains?.length)
trainsQuery.refetch().then()
}, [trains, filterTime, trainsQuery])
const nullRef = useRef(null)
let table_rows = trains.map((train) => <CSSTransition key={train.id} timeout={500} classNames="shrink" nodeRef={nullRef}>
<TrainRow train={train} tableType={tableType} date={date} time={time} />
</CSSTransition>)
return <>
<TableBody>
<TransitionGroup component={null}>
{table_rows}
</TransitionGroup>
</TableBody>
</>
}
function TrainRow({train, tableType, date, time}) {
const tripQuery = useQuery({
queryKey: ['trip', train.trip],
queryFn: () => fetch(`/api/gtfs/trip/${train.trip}/`)
.then(response => response.json()),
enabled: !!train.trip,
})
const trip = tripQuery.data ?? {}
const routeQuery = useQuery({
queryKey: ['route', trip.route],
queryFn: () => fetch(`/api/gtfs/route/${trip.route}/`)
.then(response => response.json()),
enabled: !!trip.route,
})
const route = routeQuery.data ?? {}
const trainType = getTrainType(train, trip, route)
const backgroundColor = getBackgroundColor(train, trip, route)
const textColor = getTextColor(train, trip, route)
const trainTypeDisplay = getTrainTypeDisplay(trainType)
const stopTimesQuery = useQuery({
queryKey: ['stop_times', trip.id],
queryFn: () => fetch(`/api/gtfs/stop_time/?${new URLSearchParams({trip: trip.id, order: 'stop_sequence', limit: 1000})}`)
.then(response => response.json())
.then(data => data.results),
enabled: !!trip.id,
})
const stopTimes = stopTimesQuery.data ?? []
const stopIds = stopTimes.map(stop_time => stop_time.stop)
const stopQueries = useQueries({
queries: stopIds.map(stopId => ({
queryKey: ['stop', stopId],
queryFn: () => fetch(`/api/gtfs/stop/${stopId}/`)
.then(response => response.json()),
enabled: !!stopId,
})),
})
const stops = stopTimes.map(((stopTime, i) => ({...stopTime, stop: stopQueries[i]?.data ?? {"name": "…"}}))) ?? []
let headline = stops[tableType === "departures" ? stops.length - 1 : 0]?.stop ?? {name: "Chargement…"}
const realtimeTripQuery = useQuery({
queryKey: ['realtimeTrip', trip.id, date, time],
queryFn: () => fetch(`/api/gtfs-rt/trip_update/${trip.id}/`)
.then(response => response.json()),
enabled: !!trip.id,
})
const [realtimeTripData, setRealtimeTripData] = useState({})
useEffect(() => {
if (realtimeTripQuery.data)
setRealtimeTripData(realtimeTripQuery.data)
}, [realtimeTripQuery.data])
const tripScheduleRelationship = realtimeTripData.schedule_relationship ?? 0
const realtimeQuery = useQuery({
queryKey: ['realtime', train.id, date, time],
queryFn: () => fetch(`/api/gtfs-rt/stop_time_update/${train.id}/`)
.then(response => response.json()),
enabled: !!train.id,
})
const [realtimeData, setRealtimeData] = useState({})
useEffect(() => {
if (realtimeQuery.data)
setRealtimeData(realtimeQuery.data)
}, [realtimeQuery.data])
const stopScheduleRelationship = realtimeData.schedule_relationship ?? 0
const canceled = tripScheduleRelationship === 3 || stopScheduleRelationship === 1
const delay = tableType === "departures" ? realtimeData.departure_delay : realtimeData.arrival_delay
const prettyDelay = delay && !canceled ? getPrettyDelay(delay) : ""
const [prettyScheduleRelationship, scheduleRelationshipColor] = getPrettyScheduleRelationship(tripScheduleRelationship, stopScheduleRelationship)
let stopsFilter
if (canceled)
stopsFilter = (stop_time) => true
else if (tableType === "departures")
stopsFilter = (stop_time) => stop_time.stop_sequence > train.stop_sequence && stop_time.drop_off_type === 0
else
stopsFilter = (stop_time) => stop_time.stop_sequence < train.stop_sequence && stop_time.pickup_type === 0
let stopsNames = stops.filter(stopsFilter).map(stopTime => stopTime?.stop.name ?? "").join(" > ") ?? ""
return <>
<StyledTableRow tabletype={tableType}>
<TableCell>
<div>
<Box display="flex"
justifyContent="center"
alignItems="center"
textAlign="center"
width="4em"
height="4em"
borderRadius="15%"
fontWeight="bold"
backgroundColor={backgroundColor}
color={textColor}>
{trainTypeDisplay}
</Box>
</div>
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" justifyContent="center" textAlign="center">
<div>
<div>{trip.short_name}</div>
<div>{trip.headsign}</div>
</div>
</Box>
</TableCell>
<TableCell>
<Box display="flex" alignItems="center" justifyContent="center">
<Box>
<Box fontWeight="bold" color="#FFED02" fontSize={24}>
{getDisplayTime(train, tableType)}
</Box>
<Box color={delay && delay !== "00:00:00" ? "#e86d2b" : "white"}
fontWeight={delay && delay !== "00:00:00" ? "bold" : ""}>
{prettyDelay}
</Box>
<Box color={scheduleRelationshipColor} fontWeight="bold">
{prettyScheduleRelationship}
</Box>
</Box>
</Box>
</TableCell>
<TableCell>
<Box style={{textDecoration: canceled ? 'line-through': ''}}>
<Typography fontSize={24} fontWeight="bold" data-stop-id={headline.id}>{headline.name}</Typography>
<span className="stops">{stopsNames}</span>
</Box>
</TableCell>
</StyledTableRow>
</>
}
function getTrainType(train, trip, route) {
switch (route.gtfs_feed) {
case "FR-SNCF-TGV":
case "FR-SNCF-IC":
case "FR-SNCF-TER":
let trainType = train.stop.split("StopPoint:OCE")[1].split("-")[0]
switch (trainType) {
case "Train TER":
return "TER"
case "INTERCITES":
return "INTER-CITÉS"
case "INTERCITES de nuit":
return "INTER-CITÉS de nuit"
default:
return trainType
}
case "FR-IDF-IDFM":
case "FR-GES-CTS":
return route.short_name
case "FR-EUROSTAR":
return "Eurostar"
case "IT-FRA-TI":
return "Trenitalia France"
case "ES-RENFE":
return "RENFE"
case "AT-OBB":
if (trip.short_name?.startsWith("NJ"))
return "NJ"
return "ÖBB"
case "CH-ALL":
return route.desc
default:
return trip.short_name?.split(" ")[0]
}
}
function getTrainTypeDisplay(trainType) {
switch (trainType) {
case "TGV INOUI":
return <img src="/tgv_inoui.svg" alt="TGV INOUI" width="80%" />
case "OUIGO":
return <img src="/ouigo.svg" alt="OUIGO" width="80%" />
case "ICE":
return <img src="/ice.svg" alt="ICE" width="80%" />
case "Lyria":
return <img src="/lyria.svg" alt="Lyria" width="80%" />
case "TER":
return <img src="/ter.svg" alt="TER" width="80%" />
case "Car TER":
return <div><img src="/bus.svg" alt="Car" width="40%" />
<br/>
<img src="/ter.svg" alt="TER" width="40%" /></div>
case "Eurostar":
return <img src="/eurostar_mini.svg" alt="Eurostar" width="80%" />
case "Trenitalia":
case "Trenitalia France":
return <img src="/trenitalia.svg" alt="Frecciarossa" width="80%" />
case "RENFE":
return <img src="/renfe.svg" alt="RENFE" width="80%" />
case "NJ":
return <img src="/nightjet.svg" alt="NightJet" width="80%" />
default:
return trainType
}
}
function getBackgroundColor(train, trip, route) {
let trainType = getTrainType(train, trip, route)
switch (trainType) {
case "OUIGO":
return "#0096CA"
case "Eurostar":
return "#00286A"
case "NJ":
return "#272759"
default:
if (route.color)
return `#${route.color}`
return "#FFFFFF"
}
}
function getTextColor(train, trip, route) {
if (route.text_color)
return `#${route.text_color}`
else {
let trainType = getTrainType(train, trip, route)
switch (trainType) {
case "OUIGO":
return "#FFFFFF"
case "TGV INOUI":
return "#9B2743"
case "ICE":
return "#B4B4B4"
case "INTER-CITÉS":
case "INTER-CITÉS de nuit":
return "#404042"
default:
return "#000000"
}
}
}
function getDisplayTime(train, tableType) {
let time = tableType === "departures" ? train.departure_time : train.arrival_time
let day_split = time.split(' ')
return day_split[day_split.length - 1].substring(0, 5)
}
function getPrettyDelay(delay) {
let delay_split = delay.split(':')
let hours = parseInt(delay_split[0])
let minutes = parseInt(delay_split[1])
let full_minutes = hours * 60 + minutes
return full_minutes ? `+${full_minutes} min` : "À l'heure"
}
function getPrettyScheduleRelationship(tripScheduledRelationship, stopScheduledRelationship) {
switch (tripScheduledRelationship) {
case 1:
return ["Ajouté", "#3ebb18"]
case 3:
return ["Supprimé", "#ff8701"]
default:
switch (stopScheduledRelationship) {
case 1:
return ["Supprimé", "#ff8701"]
default:
return ["", ""]
}
}
}
export default TrainsTable;

165
src/TripsFilter.js Normal file
View File

@ -0,0 +1,165 @@
import {useState} from "react"
import {
Box, Button,
Checkbox, Chip, FormControl,
FormControlLabel,
InputLabel, MenuItem, OutlinedInput, Select
} from "@mui/material"
import DirectionsBusTwoToneIcon from '@mui/icons-material/DirectionsBusTwoTone'
import SubwayTwoToneIcon from '@mui/icons-material/SubwayTwoTone'
import TrainTwoToneIcon from '@mui/icons-material/TrainTwoTone'
import TramTwoToneIcon from '@mui/icons-material/TramTwoTone'
function TripsFilter() {
const [transportModeFilter, setTransportModeFilter] = useState(
{longDistanceTrain: true, regionalTrain: true, metro: true, tram: true, bus: true})
const transportModeNames = {
train: "Trains",
longDistanceTrain: "Trains longue distance",
regionalTrain: "Trains régionaux",
metro: "Métro",
tram: "Tram",
bus: "Bus",
}
const trainCheckbox = <>
<TrainTwoToneIcon />
<Checkbox
checked={transportModeFilter.longDistanceTrain && transportModeFilter.regionalTrain}
indeterminate={transportModeFilter.longDistanceTrain !== transportModeFilter.regionalTrain}
onChange={(event) =>
setTransportModeFilter(
{...transportModeFilter, longDistanceTrain: event.target.checked, regionalTrain: event.target.checked})}
onClick={(event) => event.stopPropagation()}
/>
</>
const longDistanceTrainCheckbox = <>
<TrainTwoToneIcon />
<Checkbox
checked={transportModeFilter.longDistanceTrain}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, longDistanceTrain: event.target.checked})} />
</>
const regionalTrainCheckbox = <>
<TrainTwoToneIcon />
<Checkbox
checked={transportModeFilter.regionalTrain}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, regionalTrain: event.target.checked})} />
</>
const metroCheckbox = <>
<SubwayTwoToneIcon />
<Checkbox
checked={transportModeFilter.metro}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, metro: event.target.checked})} />
</>
const tramCheckbox = <>
<TramTwoToneIcon />
<Checkbox
checked={transportModeFilter.tram}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, tram: event.target.checked})} />
</>
const busCheckbox = <>
<DirectionsBusTwoToneIcon />
<Checkbox
checked={transportModeFilter.bus}
onChange={(event) =>
setTransportModeFilter({...transportModeFilter, bus: event.target.checked})} />
</>
// TODO Fetch routes that are accessible from one stop
// For now, we have the tram and bus routes accessible in Strasbourg main station
const routesList = [
{name: "Tous"},
{name: "A", bgColor: "#E10D19", color: "#FFFFFF"},
{name: "C", bgColor: "#F29400", color: "#FFFFFF"},
{name: "D", bgColor: "#009933", color: "#FFFFFF"},
{name: "G", bgColor: "#F6C900", color: "#000000"},
{name: "H", bgColor: "#A62341", color: "#FFFFFF"},
{name: "2", bgColor: "#FF0000", color: "#FFFFFF"},
{name: "10", bgColor: "#FFAA00", color: "#000000"},
]
const routesDict = {}
for (const route of routesList) {
routesDict[route.name] = route
}
const [selectedRoutes, setSelectedRoutes] = useState(["Tous"])
return <>
<h2>Filtres</h2>
<Box display="flex" alignItems="center" sx={{mb: 3}}>
<FormControl>
<InputLabel>Mode de transport</InputLabel>
<Select
multiple
value={selectedRoutes}
input={<OutlinedInput id="select-multiple-chip" label="Lignes" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{Object.keys(transportModeFilter).filter(key => transportModeFilter[key]).map((filterType) => (
<Chip key={filterType} label={transportModeNames[filterType]} sx={{fontWeight: "bold"}} />
))}
</Box>
)}
>
<MenuItem key="train" value="train">
<FormControlLabel label={transportModeNames["train"]} control={trainCheckbox} />
</MenuItem>
<MenuItem key="longDistanceTrain" value="longDistanceTrain">
<FormControlLabel label={transportModeNames["longDistanceTrain"]} sx={{pl: 4}} control={longDistanceTrainCheckbox} />
</MenuItem>
<MenuItem key="regionalTrain" value="regionalTrain">
<FormControlLabel label={transportModeNames["regionalTrain"]} sx={{pl: 4}} control={regionalTrainCheckbox} />
</MenuItem>
<MenuItem key="metro" value="metro">
<FormControlLabel label={transportModeNames["metro"]} control={metroCheckbox} />
</MenuItem>
<MenuItem key="tram" value="tram">
<FormControlLabel label={transportModeNames["tram"]} control={tramCheckbox} />
</MenuItem>
<MenuItem key="bus" value="bus">
<FormControlLabel label={transportModeNames["bus"]} control={busCheckbox} />
</MenuItem>
</Select>
</FormControl>
<FormControl>
<InputLabel>Ligne</InputLabel>
<Select
multiple
value={selectedRoutes}
onChange={(event) => setSelectedRoutes(event.target.value)}
input={<OutlinedInput id="select-multiple-chip" label="Lignes" />}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{selected.map(routeName => routesDict[routeName]).map((route) => (
<Chip key={route.name} label={route.name} sx={{backgroundColor: route.bgColor, color: route.color, fontWeight: "bold"}} />
))}
</Box>
)}
>
{routesList.map((route) =>
<MenuItem key={route.name} value={route.name}>
<Checkbox checked={selectedRoutes.includes(route.name)} />
<Chip label={route.name} sx={{backgroundColor: route.bgColor, color: route.color, fontWeight: "bold"}} />
</MenuItem>
)}
</Select>
</FormControl>
<Button>
Filtrer
</Button>
</Box>
</>
}
export default TripsFilter

13
src/index.css Normal file
View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

17
src/index.js Normal file
View File

@ -0,0 +1,17 @@
import React, {useMemo} from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import reportWebVitals from './reportWebVitals';
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
src/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

13
src/reportWebVitals.js Normal file
View File

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

5
src/setupTests.js Normal file
View 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';