diff --git a/README.md b/README.md
index d437cc3..871cfae 100644
--- a/README.md
+++ b/README.md
@@ -9,3 +9,8 @@ Set the environment variables found in `set_envs.sample`.
#### Important: if running the development server, prefix all `SERVER`
variables with http://. The proxy won't work otherwise and all the servers will
break
+
+- Important debug note: due to limitation set by Chrome (which doesn't
+allow cookies to be set by localhost), the login feature might not work
+properly in Chrome. Therefore, please test with other browsers (e.g.
+Safari) or put the site on a standard domain to test.
\ No newline at end of file
diff --git a/client/actions/index.js b/client/actions/index.js
index 0f6c0af..c7d402f 100644
--- a/client/actions/index.js
+++ b/client/actions/index.js
@@ -64,8 +64,136 @@ export const RESIZE_WINDOW = 'RESIZE_WINDOW';
// Logging - track data, doesn't impact rendering
export const LOG_BOUNDS = 'LOG_BOUNDS';
+// User Settings
+export const TOGGLE_TRACKING = 'TOGGLE_TRACKING';
+export const OPEN_USER_SETTINGS_PANE = 'OPEN_USER_SETTINGS_PANE';
+export const CLOSE_USER_SETTINGS_PANE = 'CLOSE_USER_SETTINGS_PANE';
+export const OPEN_PROFILE_MANAGER_PANE = 'OPEN_PROFILE_MANAGER_PANE';
+export const CLOSE_PROFILE_MANAGER_PANE = 'CLOSE_PROFILE_MANAGER_PANE';
+export const FETCHING_USER_ROUTING_PROFILE = 'FETCHING_USER_ROUTING_PROFILE';
+export const FINISHED_FETCHING_USER_ROUTING_PROFILE = 'FINISHED_FETCHING_USER_ROUTING_PROFILE';
+export const UPDATE_USER_ROUTING_PROFILE = 'UPDATE_USER_ROUTING_PROFILE';
+export const CHANGE_USER_ROUTING_PROFILE_SELECTION = 'CHANGE_USER_ROUTING_PROFILE_SELECTION';
// Action creators
+export function changeUserRoutingProfileSelection (selectedIndex) {
+ return {
+ type: CHANGE_USER_ROUTING_PROFILE_SELECTION,
+ payload: selectedIndex,
+ meta: {
+ analytics: {
+ type: 'change-user-routing-profile-selection',
+ }
+ }
+ };
+}
+
+export function refreshUserRoutingProfiles() {
+ return (dispatch) => {
+ dispatch(isFetchingUserRoutingProfiles(true));
+ const loadUserProfiles = require('../utils/api').loadUserProfiles;
+ loadUserProfiles().then(result => {
+ if (result.error != null) {
+ dispatch(updateUserRoutingProfiles(null));
+ dispatch(isFetchingUserRoutingProfiles(false));
+ return;
+ }
+ const profiles = result.data;
+ dispatch(updateUserRoutingProfiles([]));
+ dispatch(updateUserRoutingProfiles(profiles));
+ dispatch(isFetchingUserRoutingProfiles(false));
+ });
+ };
+}
+
+export function isFetchingUserRoutingProfiles(isFetching) {
+ if (isFetching) {
+ return {
+ type: FETCHING_USER_ROUTING_PROFILE,
+ meta: {
+ analytics: {
+ type: 'fetching-user-routing-profile',
+ }
+ }
+ };
+ }
+ return {
+ type: FINISHED_FETCHING_USER_ROUTING_PROFILE,
+ meta: {
+ analytics: {
+ type: 'finished-fetching-user-routing-profile',
+ }
+ }
+ };
+}
+
+export function updateUserRoutingProfiles(profiles) {
+ return {
+ type: UPDATE_USER_ROUTING_PROFILE,
+ payload: profiles,
+ meta: {
+ analytics: {
+ type: 'update-user-routing-profile',
+ }
+ }
+ };
+}
+
+export function openRoutingProfileManager() {
+ return {
+ type: OPEN_PROFILE_MANAGER_PANE,
+ meta: {
+ analytics: {
+ type: 'open-routing-profile-manager',
+ }
+ }
+ };
+}
+
+export function closeRoutingProfileManager() {
+ return {
+ type: CLOSE_PROFILE_MANAGER_PANE,
+ meta: {
+ analytics: {
+ type: 'close-routing-profile-manager',
+ }
+ }
+ };
+}
+
+export function openUserPreferences() {
+ return {
+ type: OPEN_USER_SETTINGS_PANE,
+ meta: {
+ analytics: {
+ type: 'open-user-setting',
+ }
+ }
+ };
+}
+
+export function closeUserPreferences() {
+ return {
+ type: CLOSE_USER_SETTINGS_PANE,
+ meta: {
+ analytics: {
+ type: 'close-user-setting',
+ }
+ }
+ };
+}
+
+export function toggleTracking() {
+ return {
+ type: TOGGLE_TRACKING,
+ meta: {
+ analytics: {
+ type: 'toggle-tracking',
+ }
+ }
+ };
+}
+
export function toggleTripPlanning(planningTrip) {
return (dispatch, getState) => {
const {waypoints} = getState();
diff --git a/client/components/OpenIDAuth/AuthProviderCallback.js b/client/components/OpenIDAuth/AuthProviderCallback.js
new file mode 100644
index 0000000..d2ff78f
--- /dev/null
+++ b/client/components/OpenIDAuth/AuthProviderCallback.js
@@ -0,0 +1,27 @@
+import React from "react";
+import { connect } from "react-redux";
+import { CallbackComponent } from "redux-oidc";
+import { push } from "react-router-redux";
+import userManager from "../../utils/UserManager";
+
+class CallbackPage extends React.Component {
+ render() {
+ // just redirect to '/' in both cases
+ return (
+ this.props.dispatch(push("/"))}
+ errorCallback={() => this.props.dispatch(push("/"))}
+ >
+
+
+ Redirecting...
+ If this page take too long to respond, please try logging in again or
+ contact AccessMap group.
+
+
+ );
+ }
+}
+
+export default connect()(CallbackPage);
diff --git a/client/components/OpenIDAuth/SilentRenew.js b/client/components/OpenIDAuth/SilentRenew.js
new file mode 100644
index 0000000..cd7c438
--- /dev/null
+++ b/client/components/OpenIDAuth/SilentRenew.js
@@ -0,0 +1,17 @@
+import React from "react";
+import { connect } from "react-redux";
+import { processSilentRenew } from 'redux-oidc';
+
+class SilentRenewPage extends React.Component {
+ componentWillMount () {
+ processSilentRenew();
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default connect()(SilentRenewPage);
diff --git a/client/components/OpenIDAuth/index.js b/client/components/OpenIDAuth/index.js
new file mode 100644
index 0000000..1d5e648
--- /dev/null
+++ b/client/components/OpenIDAuth/index.js
@@ -0,0 +1,130 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import userManager from '../../utils/UserManager';
+import { loadUserInfo } from '../../utils/api';
+import PropTypes from 'prop-types';
+import { DropdownMenu } from 'react-md/lib/Menus/index';
+import Avatar from 'react-md/lib/Avatars';
+import {
+ AccessibleFakeButton,
+ IconSeparator,
+} from 'react-md/lib/Helpers';
+import { FontIcon } from 'react-md/lib/FontIcons';
+import * as AppActions from '../../actions';
+import { bindActionCreators } from 'redux';
+
+function UserMenu (props) {
+ const {
+ actions,
+ user
+ } = props;
+
+ const DropdownItems = [];
+ if (user && (!user.expired)) {
+ DropdownItems.push({
+ primaryText: '[DEBUG] Show Token Info',
+ onClick: () => {
+ // TODO: delete debug prompt
+ alert(
+ JSON.stringify(user, null, 2),
+ );
+ },
+ },
+ {
+ primaryText: '[DEBUG] Get User Info with Token',
+ onClick: () => loadUserInfo(),
+ },
+ {
+ primaryText: 'Manage Routing Profiles',
+ onClick: actions.openRoutingProfileManager,
+ });
+ }
+ DropdownItems.push({
+ primaryText: 'Preferences',
+ onClick: actions.openUserPreferences,
+ },
+ {divider: true});
+ if (!user || user.expired) {
+ DropdownItems.push({
+ primaryText: 'Sign In',
+ onClick: onSignInButtonClick,
+ });
+ } else {
+ DropdownItems.push({
+ primaryText: 'Manage Account',
+ onClick: () => window.open(
+ 'https://accounts.open-to-all.com/auth/realms/OpenToAll/account',
+ '_blank'),
+ }, {
+ primaryText: 'Sign Out',
+ onClick: onSignOutButtonClick,
+ });
+ }
+
+ const AccountMenu = ({simplifiedMenu}) => (
+
+ );
+
+ AccountMenu.propTypes = {
+ simplifiedMenu: PropTypes.bool,
+ };
+
+ return ();
+}
+
+function onSignInButtonClick (event) {
+ event.preventDefault();
+ userManager.signinRedirect();
+}
+
+function onSignOutButtonClick (event) {
+ event.preventDefault();
+ userManager.removeUser(); // removes the user data from sessionStorage
+}
+
+function mapStateToProps (state) {
+ return {
+ user: state.oidc.user,
+ };
+}
+
+// function mapDispatchToProps (dispatch) {
+// return {
+// dispatch,
+// };
+// }
+
+function mapDispatchToProps (dispatch) {
+ return {
+ actions: bindActionCreators(AppActions, dispatch)
+ };
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(UserMenu);
diff --git a/client/components/Preferences/index.js b/client/components/Preferences/index.js
new file mode 100644
index 0000000..c423737
--- /dev/null
+++ b/client/components/Preferences/index.js
@@ -0,0 +1,62 @@
+/* WithScrollingContent.jsx */
+/* eslint-disable react/no-array-index-key */
+import React from 'react';
+import { connect } from 'react-redux';
+import { DialogContainer } from 'react-md/lib/Dialogs';
+import { Switch } from 'react-md/lib/SelectionControls';
+import * as AppActions from '../../actions';
+import { bindActionCreators } from 'redux';
+
+function UserPreferences (props) {
+ const {
+ actions,
+ visible,
+ preferences,
+ } = props;
+
+ const contentProps = {id: 'scrolling-content-dialog-content'};
+
+ return (
+
+
+
+ );
+}
+
+function mapStateToProps (state) {
+ return {
+ visible: state.viewVisibility.showUserSettingsPane,
+ preferences: state.userpreference,
+ };
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ actions: bindActionCreators(AppActions, dispatch)
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(UserPreferences);
\ No newline at end of file
diff --git a/client/components/RoutingProfileManager/ProfileEntry.js b/client/components/RoutingProfileManager/ProfileEntry.js
new file mode 100644
index 0000000..c92fcb1
--- /dev/null
+++ b/client/components/RoutingProfileManager/ProfileEntry.js
@@ -0,0 +1,154 @@
+import React, { PureComponent } from 'react';
+import SelectionControl from 'react-md/lib/SelectionControls';
+import InclineSlider from 'components/InclineSlider';
+import PropTypes from 'prop-types';
+import {
+ Card,
+ CardText,
+ CardTitle,
+ CardActions,
+} from 'react-md/lib/Cards';
+import TextField from 'react-md/lib/TextFields';
+import Button from 'react-md/lib/Buttons';
+import { deleteUserProfile, updateUserProfile } from '../../utils/api';
+
+export default class ProfileEntry extends PureComponent {
+ static propTypes = {
+ profileID: PropTypes.string.isRequired,
+ profileName: PropTypes.string.isRequired,
+ inclineMin: PropTypes.number.isRequired,
+ inclineMax: PropTypes.number.isRequired,
+ inclineIdeal: PropTypes.number.isRequired,
+ avoidCurbs: PropTypes.bool.isRequired,
+ avoidConstruction: PropTypes.bool.isRequired,
+ refreshList: PropTypes.func.isRequired,
+ };
+
+ constructor (props) {
+ super();
+
+ this.state = {
+ profileID: props.profileID,
+ profileName: props.profileName,
+ tempProfileName: props.profileName,
+ inclineMin: props.inclineMin,
+ tempInclineMin: props.inclineMin,
+ inclineMax: props.inclineMax,
+ tempInclineMax: props.inclineMax,
+ inclineIdeal: props.inclineIdeal,
+ avoidCurbs: props.avoidCurbs,
+ tempAvoidCurbs: props.avoidCurbs,
+ avoidConstruction: props.avoidConstruction,
+ refreshList: props.refreshList,
+ expanded: false,
+ };
+ }
+
+ render () {
+ const onSave = () => {
+ this.setState({expanded: false});
+ updateUserProfile(this.state.profileID, {
+ profileName: this.state.tempProfileName.length > 0
+ ? this.state.tempProfileName
+ : this.state.profileName,
+ inclineMin: this.state.tempInclineMin,
+ inclineMax: this.state.tempInclineMax,
+ inclineIdeal: this.state.inclineIdeal,
+ avoidCurbs: this.state.tempAvoidCurbs,
+ avoidConstruction: this.state.avoidConstruction,
+ }).then((result) => {
+ if (result.error != null) {
+ return;
+ }
+ this.state.refreshList();
+ });
+ };
+ const onCancel = () => {
+ this.setState({
+ tempProfileName: this.state.profileName,
+ tempInclineMax: this.state.inclineMax,
+ tempInclineMin: this.state.inclineMin,
+ tempAvoidCurbs: this.state.avoidCurbs,
+ expanded: false,
+ });
+ };
+ const onDelete = () => {
+ this.setState({expanded: false});
+ deleteUserProfile(this.state.profileID).then((result) => {
+ if (result.error != null) {
+ return;
+ }
+ this.state.refreshList();
+ });
+ };
+
+ const uphillSlider = (
+ this.setState({tempInclineMax: d / 100})}
+ />
+ );
+
+ const downhillSlider = (
+ this.setState({tempInclineMin: -d / 100})}
+ />
+ );
+
+ const curbrampToggle = (
+ this.setState({tempAvoidCurbs: d})}
+ />
+ );
+ return (
+ this.setState({expanded: !this.state.expanded})}
+ >
+
+
+ {
+ this.setState({tempProfileName: newName});
+ }}
+ />
+ {uphillSlider}
+ {downhillSlider}
+ {curbrampToggle}
+
+
+
+
+
+
+
+ );
+ }
+};
\ No newline at end of file
diff --git a/client/components/RoutingProfileManager/ProfileList.js b/client/components/RoutingProfileManager/ProfileList.js
new file mode 100644
index 0000000..c268e02
--- /dev/null
+++ b/client/components/RoutingProfileManager/ProfileList.js
@@ -0,0 +1,129 @@
+/* eslint-disable react/no-array-index-key */
+import React, { PureComponent } from 'react';
+import {
+ Card,
+ CardText,
+ CardTitle,
+} from 'react-md/lib/Cards';
+import { Button } from 'react-md/lib/Buttons';
+import { Collapse } from 'react-md/lib/Helpers';
+import { CircularProgress } from 'react-md/lib/Progress';
+import { loadUserProfiles } from '../../utils/api';
+import ProfileEntry from './ProfileEntry';
+import PropTypes from 'prop-types';
+
+const ACCESSIBILITY_PROPS = {
+ 'aria-busy': true,
+ 'aria-describedby': 'fake-feed-loading-progress',
+};
+
+export default class ProfileList extends PureComponent {
+ static propTypes = {
+ refreshProfileHandler: PropTypes.func.isRequired,
+ refreshStatusIndicator: PropTypes.bool.isRequired,
+ profileArray: PropTypes.array.isRequired,
+ };
+
+ constructor (props) {
+ super();
+
+ this.state = {
+ notRefreshing: true,
+ contents: props.profileArray,
+ refreshProfileHandler: props.refreshProfileHandler,
+ refreshStatusIndicator: props.refreshStatusIndicator,
+ };
+ }
+
+ componentWillReceiveProps (newProps) {
+ if (newProps.profileArray !== this.props.profileArray) {
+ this.setState({contents: newProps.profileArray});
+ }
+ }
+
+ componentWillMount () {
+ this.refreshContent();
+ }
+
+ refreshContent = () => {
+ // this.setState({notRefreshing: false});
+ //
+ // loadUserProfiles().then(result => {
+ // if (result.error != null) {
+ // this.setState({contents: null, notRefreshing: true});
+ // return;
+ // }
+ // const profiles = result.data;
+ // this.setState({contents: [], notRefreshing: true});
+ // this.setState({contents: profiles, notRefreshing: true});
+ // });
+ this.state.refreshProfileHandler();
+ };
+
+ render () {
+ const {notRefreshing, contents} = this.state;
+
+ let accessibilityProps;
+ if (!notRefreshing) {
+ accessibilityProps = ACCESSIBILITY_PROPS;
+ }
+
+ let cards;
+ if (contents == null) {
+ cards = (
+
+
+ Error loading profile.
+
+
+ );
+ } else if (contents.length < 1) {
+ cards = (
+
+
+ No Profile Found
+
+
+ );
+ } else {
+ cards = contents.map(({
+ profileID,
+ userID,
+ profileName,
+ inclineMin,
+ inclineMax,
+ inclineIdeal,
+ avoidCurbs,
+ avoidConstruction,
+ }) => (
+
+ ));
+ }
+
+ const refresh = ;
+ return (
+
+ {refresh}
+
+
+
+
+
+
+
+ );
+ }
+}
\ No newline at end of file
diff --git a/client/components/RoutingProfileManager/index.js b/client/components/RoutingProfileManager/index.js
new file mode 100644
index 0000000..d5b428f
--- /dev/null
+++ b/client/components/RoutingProfileManager/index.js
@@ -0,0 +1,61 @@
+/* eslint-disable react/no-array-index-key */
+import React from 'react';
+import { connect } from 'react-redux';
+import { DialogContainer } from 'react-md/lib/Dialogs';
+import * as AppActions from '../../actions';
+import { bindActionCreators } from 'redux';
+import ProfileList from './ProfileList';
+
+function RoutingProfileManager (props) {
+ const {
+ actions,
+ visible,
+ preferences,
+ userRoutingProfiles,
+ } = props;
+
+ const contentProps = {id: 'scrolling-content-dialog-content'};
+
+ return (
+ {}}
+ actions={[
+ {
+ label: 'Close',
+ primary: true,
+ onClick: actions.closeRoutingProfileManager,
+ }]}
+ width={800}
+ contentProps={contentProps}
+ >
+
+
+ );
+}
+
+function mapStateToProps (state) {
+ return {
+ visible: state.viewVisibility.showRoutingProfilePane,
+ userRoutingProfiles: state.userpreference.userRoutingProfiles,
+ preferences: state.userpreference,
+ };
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ actions: bindActionCreators(AppActions, dispatch),
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(RoutingProfileManager);
\ No newline at end of file
diff --git a/client/components/RoutingProfileSelector/index.js b/client/components/RoutingProfileSelector/index.js
new file mode 100644
index 0000000..32c0254
--- /dev/null
+++ b/client/components/RoutingProfileSelector/index.js
@@ -0,0 +1,92 @@
+/* eslint-disable react/no-array-index-key */
+import React from 'react';
+import { connect } from 'react-redux';
+import * as AppActions from '../../actions';
+import { bindActionCreators } from 'redux';
+
+import List from 'react-md/lib/Lists';
+import SelectField from 'react-md/lib/SelectFields';
+import Button from 'react-md/lib/Buttons';
+import { createUserProfile } from '../../utils/api';
+
+export function RoutingProfileSelector (props) {
+ const {
+ actions,
+ userRoutingProfiles,
+ currentlySelectedProfileIndex,
+ user,
+ currentActivatedProfile
+ } = props;
+
+ const profileItems = [];
+ for (let i = 0; i < userRoutingProfiles.length; i++) {
+ profileItems.push({
+ label: String(userRoutingProfiles[i].profileName),
+ value: i,
+ });
+ }
+
+ if (!user) {
+ return ();
+ }
+
+ return (
+
+ {
+ actions.changeUserRoutingProfileSelection(idx);
+ actions.setProfile({
+ profileName: userRoutingProfiles[idx].profileName,
+ inclineIdeal: -0.01,
+ inclineMax: userRoutingProfiles[idx].inclineMax,
+ inclineMin: userRoutingProfiles[idx].inclineMin,
+ requireCurbRamps: userRoutingProfiles[idx].avoidCurbs,
+ });
+ }}
+ onClick={() => actions.refreshUserRoutingProfiles()}
+ />
+
+
+ );
+}
+
+function mapStateToProps (state) {
+ return {
+ userRoutingProfiles: state.userpreference.userRoutingProfiles,
+ currentlySelectedProfileIndex: state.userpreference.currentlySelectedProfileIndex,
+ currentActivatedProfile: state.routingprofile,
+ user: state.oidc.user,
+ };
+}
+
+function mapDispatchToProps (dispatch) {
+ return {
+ actions: bindActionCreators(AppActions, dispatch),
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(RoutingProfileSelector);
\ No newline at end of file
diff --git a/client/containers/App/index.js b/client/containers/App/index.js
index fdb5763..f8b29b4 100644
--- a/client/containers/App/index.js
+++ b/client/containers/App/index.js
@@ -10,6 +10,10 @@ import Avatar from 'react-md/lib/Avatars';
import Button from 'react-md/lib/Buttons';
import Toolbar from 'react-md/lib/Toolbars';
+import UserMenu from '../../components/OpenIDAuth/';
+import UserPreferences from '../../components/Preferences'
+import RoutingProfileManager from '../../components/RoutingProfileManager'
+
import AccessMap from 'containers/AccessMap';
import FloatingButtons from 'containers/FloatingButtons';
import OmniCard from 'containers/OmniCard';
@@ -70,9 +74,7 @@ class App extends Component {
}
themed
actions={
-
- U
-
+
}
fixed
zDepth={0}
@@ -109,6 +111,8 @@ class App extends Component {
+
+
);
}
diff --git a/client/containers/OmniCard/index.js b/client/containers/OmniCard/index.js
index 4c1978c..fef771d 100644
--- a/client/containers/OmniCard/index.js
+++ b/client/containers/OmniCard/index.js
@@ -24,6 +24,8 @@ import CaneUserIcon from 'components/Icons/CaneUserIcon';
import PoweredWheelchairIcon from 'components/Icons/PoweredWheelchairIcon';
import WheelchairIcon from 'components/Icons/WheelchairIcon';
+import RoutingProfileSelector from 'components/RoutingProfileSelector';
+
import './style.scss';
const OmniCard = (props) => {
@@ -247,6 +249,7 @@ const OmniCard = (props) => {
>
settings
+
);
diff --git a/client/index.js b/client/index.js
index 203bc0d..aedcc2c 100644
--- a/client/index.js
+++ b/client/index.js
@@ -6,16 +6,21 @@ import './style.scss';
import './fonts/index.css';
import './index.html';
+import { OidcProvider } from 'redux-oidc';
+import userManager from './utils/UserManager';
+import Routes from './routes'
+
// Note: order matters here (at least with webpack as of 2017-05-22).
// If styles + html get imported after App, component-level styling breaks
/* eslint-disable import/first */
-import App from 'containers/App';
import store from 'store';
/* eslint-enable import/first */
ReactDOM.render(
-
+
+
+
,
document.getElementById('root')
);
diff --git a/client/reducers/defaults.js b/client/reducers/defaults.js
index f0704e7..160471b 100644
--- a/client/reducers/defaults.js
+++ b/client/reducers/defaults.js
@@ -53,3 +53,16 @@ export const defaultMode = 'UPHILL';
export const defaultBrowser = {
mediaType: null,
};
+
+export const defaultUserPreferences = {
+ showUserSettingsPane: false,
+ enableTracking: true,
+ routingProfiles:[],
+ currentlySelectedProfileIndex: -1,
+ fetchingUserRoutingProfiles: false,
+};
+
+export const defaultViewVisbility = {
+ showUserSettingsPane: false,
+ showRoutingProfileManager: false,
+};
\ No newline at end of file
diff --git a/client/reducers/index.js b/client/reducers/index.js
index 04add39..9a1e47d 100644
--- a/client/reducers/index.js
+++ b/client/reducers/index.js
@@ -11,6 +11,11 @@ import tripplanning from './tripplanning';
import view from './view';
import waypoints from './waypoints';
+import { routerReducer } from 'react-router-redux';
+import { reducer as oidcReducer } from 'redux-oidc';
+import userPreference from './user-preference';
+import viewVisibility from './view-visibility';
+
/**
* Routing to be implemented
*/
@@ -25,4 +30,8 @@ export default combineReducers({
tripplanning,
view,
waypoints,
+ routing: routerReducer,
+ oidc: oidcReducer,
+ userpreference: userPreference,
+ viewVisibility: viewVisibility,
});
diff --git a/client/reducers/routing-profile.js b/client/reducers/routing-profile.js
index 4f0c58f..160c5d2 100644
--- a/client/reducers/routing-profile.js
+++ b/client/reducers/routing-profile.js
@@ -34,6 +34,8 @@ const handleInclineIdeal = (state = defaults.inclineIdeal, action) => {
case 'cane':
case 'custom':
return -0.01
+ default:
+ return action.payload.inclineIdeal;
}
case SET_INCLINE_IDEAL:
return action.payload;
@@ -55,6 +57,7 @@ const handleInclineMax = (state = defaults.inclineMax, action) => {
case 'custom':
return state;
default:
+ return action.payload.inclineMax;
}
case SET_INCLINE_MAX:
return action.payload;
@@ -75,6 +78,8 @@ const handleInclineMin = (state = defaults.inclineMin, action) => {
profiles.cane.inclineMin;
case 'custom':
return state;
+ default:
+ return action.payload.inclineMin;
}
case SET_INCLINE_MIN:
return action.payload;
@@ -95,6 +100,8 @@ const handleCurbRamps = (state = defaults.requireCurbRamps, action) => {
return false
case 'custom':
return state;
+ default:
+ return action.payload.requireCurbRamps;
}
case TOGGLE_CURBRAMPS:
return !state;
diff --git a/client/reducers/user-preference.js b/client/reducers/user-preference.js
new file mode 100644
index 0000000..5f36f46
--- /dev/null
+++ b/client/reducers/user-preference.js
@@ -0,0 +1,57 @@
+import {
+ TOGGLE_TRACKING,
+ UPDATE_USER_ROUTING_PROFILE,
+ FETCHING_USER_ROUTING_PROFILE,
+ FINISHED_FETCHING_USER_ROUTING_PROFILE,
+ CHANGE_USER_ROUTING_PROFILE_SELECTION,
+} from 'actions';
+
+import { defaultUserPreferences as defaults } from './defaults';
+import { combineReducers } from 'redux';
+
+function handleUserTracking (state = defaults.enableTracking, action) {
+ switch (action.type) {
+ case TOGGLE_TRACKING:
+ return !state;
+ default:
+ return state;
+ }
+}
+
+function handleUpdateUserRoutingProfiles (
+ state = defaults.routingProfiles, action) {
+ switch (action.type) {
+ case UPDATE_USER_ROUTING_PROFILE:
+ return action.payload;
+ default:
+ return state;
+ }
+}
+
+function handleFetchingUserRoutingProfiles (
+ state = defaults.fetchingUserRoutingProfiles, action) {
+ switch (action.type) {
+ case FETCHING_USER_ROUTING_PROFILE:
+ return true;
+ case FINISHED_FETCHING_USER_ROUTING_PROFILE:
+ return false;
+ default:
+ return state;
+ }
+}
+
+function handleChangeUserRoutingProfileSelection (
+ state = defaults.fetchingUserRoutingProfiles, action) {
+ switch (action.type) {
+ case CHANGE_USER_ROUTING_PROFILE_SELECTION:
+ return action.payload;
+ default:
+ return state;
+ }
+}
+
+export default combineReducers({
+ enableTracking: handleUserTracking,
+ userRoutingProfiles: handleUpdateUserRoutingProfiles,
+ fetchingUserRoutingProfiles: handleFetchingUserRoutingProfiles,
+});
\ No newline at end of file
diff --git a/client/reducers/user-settings.js b/client/reducers/user-settings.js
new file mode 100644
index 0000000..91b497b
--- /dev/null
+++ b/client/reducers/user-settings.js
@@ -0,0 +1,16 @@
+import {
+ SET_TRACKING
+} from 'actions';
+
+import { defaultUserSettings as defaults } from './defaults';
+
+export default function handle(state = defaults, action) {
+ switch (action.type) {
+ case SET_TRACKING:
+ return {
+ track: action.payload
+ };
+ default:
+ return state;
+ }
+}
diff --git a/client/reducers/view-visibility.js b/client/reducers/view-visibility.js
new file mode 100644
index 0000000..06f06c3
--- /dev/null
+++ b/client/reducers/view-visibility.js
@@ -0,0 +1,35 @@
+import {
+ OPEN_USER_SETTINGS_PANE,
+ CLOSE_USER_SETTINGS_PANE,
+ OPEN_PROFILE_MANAGER_PANE,
+ CLOSE_PROFILE_MANAGER_PANE
+} from 'actions';
+import { defaultViewVisbility as defaults } from './defaults';
+import { combineReducers } from 'redux';
+
+function handleUserSettingsPane(state = defaults.showUserSettingsPane, action) {
+ switch (action.type) {
+ case OPEN_USER_SETTINGS_PANE:
+ return true;
+ case CLOSE_USER_SETTINGS_PANE:
+ return false;
+ default:
+ return state;
+ }
+}
+
+function handleRoutingProfileManagerPane(state = defaults.showRoutingProfileManager, action) {
+ switch (action.type) {
+ case OPEN_PROFILE_MANAGER_PANE:
+ return true;
+ case CLOSE_PROFILE_MANAGER_PANE:
+ return false;
+ default:
+ return state;
+ }
+}
+
+export default combineReducers({
+ showUserSettingsPane: handleUserSettingsPane,
+ showRoutingProfilePane: handleRoutingProfileManagerPane
+});
\ No newline at end of file
diff --git a/client/routes/index.js b/client/routes/index.js
new file mode 100644
index 0000000..1b2b832
--- /dev/null
+++ b/client/routes/index.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Router, Route, browserHistory } from 'react-router';
+import store from '../store';
+import { syncHistoryWithStore } from 'react-router-redux';
+import App from '../containers/App';
+import AuthProviderCallbackPage from '../components/OpenIDAuth/AuthProviderCallback';
+import SilentRenewPage from '../components/OpenIDAuth/SilentRenew';
+
+const history = syncHistoryWithStore(browserHistory, store);
+
+export default function Routes(props) {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/client/store/index.js b/client/store/index.js
index c05850f..6eecbbd 100644
--- a/client/store/index.js
+++ b/client/store/index.js
@@ -1,4 +1,8 @@
import { createStore, applyMiddleware } from 'redux';
+import { loadUser } from "redux-oidc";
+import userManager from "../utils/UserManager";
+import { browserHistory } from "react-router";
+import { routerMiddleware } from "react-router-redux";
import thunkMiddleware from 'redux-thunk';
import analytics from 'redux-analytics';
@@ -12,7 +16,25 @@ middlewares.push(thunkMiddleware);
/* eslint-disable global-require */
if (process.env.NODE_ENV === 'development') {
const { logger } = require('redux-logger');
+
+ // Rakam analytics support - using npm package appears to be uncommon, but
+ // is nice for consistency and bundling
+ rakam.init(`${process.env.ANALYTICS_KEY}`, null, {
+ apiEndpoint:`${process.env.ANALYTICS_SERVER}`,
+ includeUtm: true,
+ trackClicks: true,
+ trackForms: true,
+ includeReferrer: true
+ });
+
+ const analyticsMiddleware = analytics(({ type, payload }, state) => {
+ if (state.userpreference.enableTracking || process.env.NODE_ENV === 'development') {
+ rakam.logEvent(type, { ...state.analytics, ...payload });
+ }
+ });
+
middlewares.push(logger);
+ middlewares.push(analyticsMiddleware);
}
//
@@ -43,9 +65,12 @@ if (useAnalytics) {
}
+middlewares.push(routerMiddleware(browserHistory));
+
const store = createStore(
rootReducer,
applyMiddleware(...middlewares)
);
+loadUser(store, userManager);
export default store;
diff --git a/client/utils/UserManager.js b/client/utils/UserManager.js
new file mode 100644
index 0000000..461c4bb
--- /dev/null
+++ b/client/utils/UserManager.js
@@ -0,0 +1,19 @@
+import { createUserManager } from 'redux-oidc';
+import { WebStorageStateStore } from 'oidc-client'
+
+const userManagerConfig = {
+ client_id: 'test-dev',
+ redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/callback`,
+ response_type: 'token id_token',
+ scope: 'openid profile offline_access',
+ authority: 'https://accounts.open-to-all.com/auth/realms/OpenToAll',
+ silent_redirect_uri: `${window.location.protocol}//${window.location.hostname}${window.location.port ? `:${window.location.port}` : ''}/silent_renew`,
+ automaticSilentRenew: true,
+ filterProtocolClaims: true,
+ loadUserInfo: true,
+ userStore: new WebStorageStateStore({ store: window.localStorage })
+};
+
+const userManager = createUserManager(userManagerConfig);
+
+export default userManager;
\ No newline at end of file
diff --git a/client/utils/api.js b/client/utils/api.js
new file mode 100644
index 0000000..381f9fa
--- /dev/null
+++ b/client/utils/api.js
@@ -0,0 +1,60 @@
+import store from "../store";
+
+export function loadUserInfo() {
+ const url =
+ "https://accounts.open-to-all.com/auth/realms/OpenToAll/protocol/openid-connect/userinfo";
+
+ return apiRequest(url).then(result => {
+ const note = "\n\nNote: If the auth token expires, then you won't be able to " +
+ "see the any user info above. This could be caused by automatic renew " +
+ "not working properly in this browser.";
+ alert(JSON.stringify(result.data, null, 2) + note);
+ });
+}
+
+export function loadUserProfiles() {
+ const url =
+ "http://localhost:4040/api/profiles";
+
+ return apiRequest(url);
+}
+
+export function deleteUserProfile(profileID) {
+ const url =
+ `http://localhost:4040/api/profile?profileID=${profileID}`;
+
+ return apiRequest(url, "DELETE");
+}
+
+export function updateUserProfile(profileID, newProfile) {
+ const url =
+ `http://localhost:4040/api/profile?profileID=${profileID}`;
+
+ return apiRequest(url, "PUT", newProfile);
+}
+
+export function createUserProfile(newProfile) {
+ const url =
+ `http://localhost:4040/api/profile`;
+
+ return apiRequest(url, "POST", newProfile);
+}
+
+// a request helper which reads the access_token from the redux state and passes it in its HTTP request
+function apiRequest(url, method = "GET", body = undefined) {
+ const token = store.getState().oidc.user.access_token;
+ const headers = new Headers();
+ headers.append("Content-Type", "application/json");
+ headers.append("Authorization", `Bearer ${token}`);
+
+ const options = {
+ method,
+ headers,
+ body: body ? JSON.stringify(body) : undefined,
+ };
+
+ return fetch(url, options)
+ .then(res => res.json())
+ .then(data => ({ data }))
+ .catch(error => ({ error }));
+}
diff --git a/package.json b/package.json
index b6cf894..3db9366 100644
--- a/package.json
+++ b/package.json
@@ -43,11 +43,13 @@
"@turf/bbox-polygon": "4.7.3",
"@turf/inside": "^4.5.2",
"chroma-js": "^1.3.3",
+ "classnames": "^2.2.5",
"immutable": "^3.8.1",
"lodash.throttle": "^4.1.1",
"mapbox": "^1.0.0-beta7",
"mapbox-gl": "0.42.2",
"node-sass": "^4.5.3",
+ "oidc-client": "^1.3.0",
"prop-types": "15.5.10",
"rakam-js": "^1.0.3",
"react": "^16.2.0",
@@ -56,10 +58,14 @@
"react-mapbox-gl": "^3.5.1",
"react-md": "^1.2.11",
"react-redux": "^5.0.6",
+ "react-router": "^3.2.0",
+ "react-router-redux": "^4.0.8",
"react-transition-group": "^2.2.1",
"redux": "3.6.0",
"redux-analytics": "^0.3.1",
"redux-logger": "^3.0.6",
+ "redux-oidc": "^3.0.0-beta.14",
+ "redux-saga": "^0.10.4",
"redux-thunk": "^2.2.0",
"sass-loader": "^6.0.5",
"transform-loader": "^0.2.4"