Commit 61493e5a authored by Florent Chehab's avatar Florent Chehab

Merge branch 'frontend' into 'master'

Frontend update

See merge request chehabfl/outgoing_rex!29
parents 9936114a 5f18472b
Pipeline #26901 passed with stages
in 3 minutes and 6 seconds
......@@ -64,6 +64,6 @@
63,"Technical University Of Kosice","Kosice","SK","48.73280395","21.244194264458",,,
64,"Chalmers University Of Technology","Goteborg","SE","57.6896523","11.9766811023544",,,
65,"Lulea University Of Technology","Lulea","SE","65.6170445","22.1370606335398",,,
66,"Ecole Polytechnique Federale De Lausanne","Lausanne","CH","46.5186594","6.566561505148","EPFL","https://www.epfl.ch/","https://www.epfl.ch/img/epfl_small.png"
66,"École Polytechnique Fédérale De Lausanne","Lausanne","CH","46.5186594","6.566561505148","EPFL","https://www.epfl.ch/","https://upload.wikimedia.org/wikipedia/commons/f/f4/Logo_EPFL.svg"
67,"National Chiao Tung University","Hsinchu","TW","24.78676765","120.997244116807",,,
68,"National Taiwan University Of Science And Technology","Taipei","TW","25.01350785","121.541707560048",,,
......@@ -18,7 +18,7 @@ class LoadUniversities(LoadGeneric):
destinations_path = os.path.abspath(tmp)
data = pd.read_csv(destinations_path, sep=',', header=0,
dtype=object)
dtype=object).fillna('')
for index, row in data.iterrows():
utc_id, univ_name, city_name, country_code, lat, lon, acronym, website, logo = row
......
../../../node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css
\ No newline at end of file
../../../node_modules/leaflet.markercluster/dist/MarkerCluster.css
\ No newline at end of file
../../../node_modules/leaflet/dist/images
\ No newline at end of file
../../../node_modules/leaflet/dist/leaflet.js
\ No newline at end of file
../../../node_modules/leaflet.markercluster/dist/leaflet.markercluster.js
\ No newline at end of file
export const SAVE_MAIN_MAP_POSITION = 'SAVE_MAIN_MAP_POSITION';
export const SAVE_SELECTED_UNIVERSITIES = 'SAVE_SELECTED_UNIVERSITIES';
export const SAVE_FILTER_CONFIG = 'SAVE_FILTER_CONFIG';
import {
SAVE_SELECTED_UNIVERSITIES,
SAVE_FILTER_CONFIG
} from "./action-types";
export function saveSelectedUniversities(new_selection) {
return {
type: SAVE_SELECTED_UNIVERSITIES,
new_selection
};
}
export function saveFilterConfig(config) {
return {
type: SAVE_FILTER_CONFIG,
config
};
}
......@@ -4,10 +4,10 @@ import {
} from "./action-types";
export function saveMainMapPosition(new_position) {
export function saveMainMapPosition(new_position) {
return {
type: SAVE_MAIN_MAP_POSITION,
new_position
};
}
import {
SAVE_SELECTED_UNIVERSITIES,
SAVE_FILTER_CONFIG
} from "./action-types";
export function saveSelectedUniversities(new_selection) {
return {
type: SAVE_SELECTED_UNIVERSITIES,
new_selection
};
}
export function saveFilterConfig(config) {
return {
type: SAVE_FILTER_CONFIG,
config
};
}
......@@ -22,17 +22,19 @@ import MyComponent from './MyComponent'
// import route Components here
import {
BrowserRouter as Router,
Route,
} from 'react-router-dom';
import {
countriesFetchData,
currenciesFetchData,
} from '../generated/actions';
import PageMap from './pages/PageMap';
import PageHome from './pages/PageHome';
import PageFilter from './pages/PageFilter';
import PageSearch from './pages/PageSearch';
const drawerWidth = 240;
......@@ -114,7 +116,7 @@ class App extends MyComponent {
};
myRender() {
const { classes } = this.props;
return (
......@@ -157,8 +159,9 @@ class App extends MyComponent {
<main className={classes.content}>
<Route path="/app/" exact={true} component={PageHome} />
<Route path="/app/search" component={PageSearch} />
<Route path="/app/map" component={PageMap} />
<Route path="/app/filter" component={PageFilter} />
</main>
</div>
......@@ -174,14 +177,16 @@ App.propTypes = {
const mapStateToProps = (state) => {
return {
countries: state.countries
countries: state.countries,
currencies: state.currencies
}
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: {
countries: () => dispatch(countriesFetchData())
countries: () => dispatch(countriesFetchData()),
currencies: () => dispatch(currenciesFetchData()),
}
};
};
......
......@@ -3,6 +3,30 @@ import Loading from './other/Loading';
class MyComponent extends Component {
getFetchedData(prop) {
return this.props[prop].fetched.data;
}
getAllFetchedData() {
let out = Object()
for (let prop_key in this.props) {
let prop = this.props[prop_key];
if (prop && 'fetched' in prop) {
out[prop_key] = prop.fetched.data;
}
}
return out;
}
joinCampus(campus) {
const { universities, countries, cities } = this.getAllFetchedData();
let res = Object.assign({}, campus); //copy for safety
res.university = universities[campus.university];
res.city = cities[campus.city]
res.country = countries[res.city.country]
return res;
}
checkProps(val) {
for (let el in this.props) {
let prop = this.props[el];
......@@ -25,12 +49,12 @@ class MyComponent extends Component {
return this.checkProps('invalidated');
}
loadPropsIfNeeded(){
loadPropsIfNeeded() {
for (let prop_key in this.props) {
let prop = this.props[prop_key];
if (prop && 'fetched' in prop){
if ( (!prop.fetched.fetchedAt) || prop.invalidated){
if (!prop.isLoading){
if (prop && 'fetched' in prop) {
if ((!prop.fetched.fetchedAt) || prop.invalidated) {
if (!prop.isLoading) {
this.props.fetchData[prop_key]();
}
}
......@@ -42,14 +66,14 @@ class MyComponent extends Component {
this.loadPropsIfNeeded();
this.myComponentDidMount();
}
myComponentDidMount(){};
myComponentDidMount() { };
componentDidUpdate() {
// TODO ajouter expire date
this.loadPropsIfNeeded();
this.myComponentDidMount();
}
myComponentDidMount(){};
myComponentDidMount() { };
render() {
if (this.checkPropsHasError()) {
......
// Inspired by : https://material-ui.com/demos/autocomplete/
import React from 'react';
import PropTypes from 'prop-types';
import keycode from 'keycode';
import Downshift from 'downshift';
import { withStyles } from '@material-ui/core/styles';
import TextField from '@material-ui/core/TextField';
import Paper from '@material-ui/core/Paper';
import MenuItem from '@material-ui/core/MenuItem';
import Chip from '@material-ui/core/Chip';
import _ from 'underscore';
import fuzzysort from 'fuzzysort';
function renderInput(inputProps) {
const { InputProps, classes, ref, ...other } = inputProps;
return (
<TextField
InputProps={{
inputRef: ref,
classes: {
root: classes.inputRoot,
},
...InputProps,
}}
{...other}
/>
);
}
function renderSuggestion({ suggestion, index, itemProps, highlightedIndex, selectedItem }) {
const isHighlighted = highlightedIndex === index;
const isSelected = (selectedItem || '').indexOf(suggestion) > -1;
return (
<MenuItem
{...itemProps}
key={suggestion.label}
selected={isHighlighted}
component="div"
style={{
fontWeight: isSelected ? 500 : 400,
}}
>
{suggestion.label}
</MenuItem>
);
}
renderSuggestion.propTypes = {
highlightedIndex: PropTypes.number,
index: PropTypes.number,
itemProps: PropTypes.object,
selectedItem: PropTypes.string,
suggestion: PropTypes.shape({ label: PropTypes.string, id: PropTypes.number }).isRequired,
};
class DownshiftMultiple extends React.Component {
state = this.props.config;
componentDidMount() {
this.setState(this.props.config);
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (this.state.selectedItems != prevState.selectedItems) {
this.props.onChange(this.state.selectedItems);
}
}
componentWillUnmount() {
if (this.props.onComponentUnmount) {
this.props.onComponentUnmount(this.state)
}
}
getSuggestions(value) {
const { options } = this.props;
const { selectedItems } = this.state;
let possible = _.difference(options, selectedItems);
const filter = fuzzysort.go(value, possible, { limit: 5, key: 'label' });
if (filter.length > 0){
return _.map(filter, (item) => item.obj)
} else {
return possible.slice(0,4)
}
}
handleKeyDown = event => {
const { inputValue, selectedItems } = this.state;
if (selectedItems.length && !inputValue.length && keycode(event) === 'backspace') {
this.setState({
selectedItems: selectedItems.slice(0, selectedItems.length - 1),
});
}
};
handleInputChange = event => {
this.setState({ inputValue: event.target.value });
};
handleChange = item => {
const { options } = this.props;
let { selectedItems } = this.state;
if (selectedItems.indexOf(item) === -1) {
for (let ind in options) {
let el = options[ind]
if (el.id == item) {
selectedItems = [...selectedItems, el];
break;
}
}
}
this.setState({
inputValue: '',
selectedItems,
});
};
handleDelete = item => () => {
this.setState(state => {
const selectedItems = [...state.selectedItems];
selectedItems.splice(selectedItems.indexOf(item), 1);
return { selectedItems };
});
};
render() {
const { classes, field_label, field_placeholder } = this.props;
const { inputValue, selectedItems } = this.state;
return (
<div className={classes.root}>
<Downshift
id="downshift-multiple"
inputValue={inputValue}
onChange={this.handleChange}
selectedItem={selectedItems}
>
{({
getInputProps,
getItemProps,
isOpen,
inputValue: inputValue2,
selectedItem: selectedItem2,
highlightedIndex,
}) => (
<div className={classes.container}>
{renderInput({
fullWidth: true,
classes,
InputProps: getInputProps({
startAdornment: selectedItems.map(item => (
<Chip
key={item.id}
tabIndex={-1}
label={item.label}
className={classes.chip}
onDelete={this.handleDelete(item.id)}
variant="outlined"
color="primary"
/>
)),
onChange: this.handleInputChange,
onKeyDown: this.handleKeyDown,
placeholder: field_placeholder,
}),
label: field_label,
})}
{isOpen ? (
<Paper className={classes.paper} square>
{this.getSuggestions(inputValue2).map((suggestion, index) =>
renderSuggestion({
suggestion,
index,
itemProps: getItemProps({ item: suggestion.id }),
highlightedIndex,
selectedItem: selectedItem2,
}),
)}
</Paper>
) : null}
</div>
)}
</Downshift>
</div>
);
}
}
DownshiftMultiple.propTypes = {
classes: PropTypes.object.isRequired,
};
DownshiftMultiple.defaultProps = {
options: [
{ label: 'Item 1', id: 1 },
{ label: 'Item 2', id: 2 },
{ label: 'Item 3', id: 3 },
{ label: 'Item 4', id: 4 },
],
field_label: "label",
field_placeholder: "placeholder",
config: { selectedItems: [], inputValue: '' }
};
const styles = theme => ({
root: {
flexGrow: 1,
height: 250,
},
container: {
flexGrow: 1,
position: 'relative',
},
paper: {
position: 'absolute',
zIndex: 1,
marginTop: theme.spacing.unit,
left: 0,
right: 0,
},
chip: {
margin: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 4}px`,
},
inputRoot: {
flexWrap: 'wrap',
}
});
export default withStyles(styles)(DownshiftMultiple);
import React from 'react';
import DownshiftMultiple from './DownshiftMultiple';
import MyComponent from '../MyComponent'
import { connect } from "react-redux";
import _ from 'underscore';
import { saveSelectedUniversities, saveFilterConfig } from '../../actions/filter';
import {
universitiesFetchData,
mainCampusesFetchData,
citiesFetchData,
countriesFetchData
} from '../../generated/actions';
class Filter extends MyComponent {
saveContriesFilterConfig(state) {
this.props.saveConfig({ contriesFilter: state })
}
// getUnivFromCampus(campus) {
// const { universities } = this.props;
// return universities[campus.univ]
// }
// getCountryFromUniversity(univ) {
// const { countries } = this.props;
// return countries[univ.country]
// }
// getCountryFromCampus(campus) {
// const univ = this.getUnivFromCampus(campus);
// return this.getCountryFromUniversity(univ);
// }
getCountriesWhereThereAreUniversities() {
const { mainCampuses } = this.getAllFetchedData();
let res = [];
_.each(mainCampuses, (campus) => {
const campusFull = this.joinCampus(campus)
res.push(campusFull.country)
});
return _.uniq(res, false, (c) => { return c.iso_alpha2_code });
}
updateSelectedUniversities(selection) {
const { mainCampuses } = this.getAllFetchedData();
const listOfCountries = _.map(selection, (s) => s.id);
let selected_universities = []
_.each(mainCampuses, (campus) => {
const campusFull = this.joinCampus(campus)
if (_.indexOf(listOfCountries, campusFull.country.iso_alpha2_code) > -1) {
selected_universities.push(campusFull.university.id);
}
})
this.props.saveSelection(selected_universities);
}
myRender() {
const options = _.map(this.getCountriesWhereThereAreUniversities(),
(c) => { return { id: c.iso_alpha2_code, label: c.name } })
return (
<DownshiftMultiple
options={options}
onChange={(selection) => this.updateSelectedUniversities(selection)}
onComponentUnmount={(state) => this.saveContriesFilterConfig(state)}
config={this.props.contriesFilterConfig}
/>
);
}
}
const mapStateToProps = (state) => {
return {
universities: state.universities,
mainCampuses: state.mainCampuses,
cities: state.cities,
countries: state.countries,
contriesFilterConfig: state.app.filter.contriesFilter
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: {
universities: () => dispatch(universitiesFetchData()),
mainCampuses: () => dispatch(mainCampusesFetchData()),
cities: () => dispatch(citiesFetchData()),
countries: () => dispatch(countriesFetchData())
},
saveSelection: (selectedUniversities) => dispatch(saveSelectedUniversities(selectedUniversities)),
saveConfig: (config) => dispatch(saveFilterConfig(config))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Filter);
import React, { Component } from 'react';
import CardMedia from '@material-ui/core/CardMedia';
class MyCardMedia extends Component {
render() {
const { title, height, url } = this.props;
if (url == "") {
return (<div/>);
}
return (
<CardMedia
component="img"
height={height}
image={url}
title={title}
/>
);
}
}
export default MyCardMedia;
\ No newline at end of file
import React, { Component } from 'react';
import React, { Component, forwardRef } from 'react';
import { connect } from "react-redux";
import Loading from '../other/Loading';
import { Map, TileLayer, Marker, Popup, LayersControl, FeatureGroup, Circle, LayerGroup } from 'react-leaflet';
import { Marker, Popup } from 'react-leaflet';
// import MarkerClusterGroup from 'react-leaflet-markercluster';
import UnivPopupContent from './UnivPopupContent';
import {
universitiesFetchData,
universitiesInvalidated,
mainCampusesFetchData,
mainCampusesInvalidated
citiesFetchData,
countriesFetchData
} from '../../generated/actions';
import MyComponent from '../MyComponent'
class UnivMarkers extends MyComponent {
class UnivMarkers extends Component {
render() {
let universities = this.props.universities.fetched.data;
let mainCampuses = this.props.mainCampuses.fetched.data;
myRender() {
let { universities,
mainCampuses,
countries,
cities } = this.getAllFetchedData()
let selected_main_campus = [];
for (let main_campus_pk in mainCampuses) {
let campus = mainCampuses[main_campus_pk]
let univ = universities[campus.university]
if (univ && campus) {
let city = cities[campus.city]
let country = countries[city.country]
selected_main_campus.push({
univ_name: univ.name,
univ_logo: univ.logo,
univ_city: city.name,
univ_country: country.name,
lat: campus.lat,
lon: campus.lon,
id: univ.id
......@@ -37,8 +43,13 @@ class UnivMarkers extends Component {
return (
selected_main_campus.map((el) => (
<Marker key={el.id} position={[el.lat, el.lon]}>
<Popup>
{el.univ_name}
<Popup closeButton={false} >
<UnivPopupContent
name={el.univ_name}
logo={el.univ_logo}
city={el.univ_city}
country={el.univ_country}
/>
</Popup>
</Marker>
))
......@@ -51,8 +62,21 @@ class UnivMarkers extends Component {
const mapStateToProps = (state) => {
return {
universities: state.universities,
mainCampuses: state.mainCampuses
mainCampuses: state.mainCampuses,
cities: state.cities,
countries: state.countries
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: {
universities: () => dispatch(universitiesFetchData()),
mainCampuses: () => dispatch(mainCampusesFetchData()),
cities: () => dispatch(citiesFetchData()),
countries: () => dispatch(countriesFetchData())
}
};
};
export default connect(mapStateToProps)(UnivMarkers);
export default connect(mapStateToProps, mapDispatchToProps)(UnivMarkers);
......@@ -3,10 +3,6 @@ import MyComponent from '../MyComponent'
import { connect } from "react-redux";
import { Map, TileLayer, LayersControl, LayerGroup } from 'react-leaflet';
import {
universitiesFetchData,
mainCampusesFetchData,
} from '../../generated/actions';
import UnivMarkers from './UnivMakers';
import { saveMainMapPosition } from '../../actions/map'
......@@ -105,18 +101,12 @@ class UnivMap extends MyComponent {
const mapStateToProps = (state) => {
return {
universities: state.universities,
mainCampuses: state.mainCampuses,
map: state.app.mainMap
};
};
const mapDispatchToProps = (dispatch) => {
return {
fetchData: {
universities: () => dispatch(universitiesFetchData()),
mainCampuses: () => dispatch(mainCampusesFetchData())
},