Skip to content

Commit

Permalink
Merge pull request #15 from binary-butterfly/sidebar
Browse files Browse the repository at this point in the history
Add Sidebar
  • Loading branch information
DysphoricUnicorn authored Jan 25, 2022
2 parents 7b8d59c + 38a7c43 commit a62c5ec
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 273 deletions.
2 changes: 1 addition & 1 deletion assets/js/Root.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const Root = () => {
banana: true,
hours: {
sunday: [{from: ['7', '0'], until: ['22', '0']}],
monday: [{from: ['7', '0'], until: ['22', '0']}],
monday: [{from: ['7', '0'], until: ['15', '0']}],
tuesday: [{from: ['20', '0'], until: ['22', '0']}],
wednesday: [{from: ['7', '0'], until: ['22', '0']}],
thursday: [{from: ['7', '0'], until: ['22', '0']}],
Expand Down
178 changes: 129 additions & 49 deletions assets/js/components/ButterflyMap.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import ownCss from '../../css/ButterflyMap.css'; // NOT unused.
import css from 'maplibre-gl/dist/maplibre-gl.css'; // NOT unused either.

import PropTypes from 'prop-types';
import React from 'react';
import Button from './Button';
import ControlBar from './ControlBar';
import PointBar from './PointBar';
import styled, {ThemeProvider} from 'styled-components';
import ReactMapGL, {AttributionControl, FlyToInterpolator, Marker} from 'react-map-gl';
import {localStringsPropTypes, dayPropTypes, hoursPropTypes} from '../data/propTypes';
import {localStringsPropTypes, hoursPropTypes} from '../data/propTypes';
import {filterHours} from '../data/helpers';

import ownCss from '../../css/ButterflyMap.css'; // NOT unused.
import css from 'maplibre-gl/dist/maplibre-gl.css'; // NOT unused either.
import {MapSidebar, SidebarContent} from './Sidebar';

const PointerBox = styled.div`
cursor: pointer;
Expand All @@ -19,15 +20,24 @@ const CenterMapButton = styled(Button)`
margin: 0.5rem 0 0.5rem 0;
`;

const MapAndBarContainer = styled.div`
display: flex;
`;

const MapContainer = styled.div`
width: 100%;
z-index: 0;
`;

const Markers = React.memo((props) => {
// Use memo to avoid needless re-renders of the markers
const {displayPointTypes, doMapMove} = props;
const {displayPointTypes, handleMapMarkerClick} = props;

return <>
{displayPointTypes.map((pointType) => {
return pointType.points.map((point, index) =>
<Marker key={index} {...point.position}
onClick={() => doMapMove(point.position)}
onClick={() => handleMapMarkerClick(point.position)}
id={'react-butterfly-map-pointer' + pointType.name + index}
getCursor={() => 'pointer'}
>
Expand All @@ -42,10 +52,14 @@ const Markers = React.memo((props) => {
});

Markers.propTypes = {
doMapMove: PropTypes.func.isRequired,
handleMapMarkerClick: PropTypes.func.isRequired,
displayPointTypes: PropTypes.array.isRequired,
};

const calculateDistance = (position, to) => {
return Math.abs(position.latitude - to.latitude) + Math.abs(position.longitude - to.longitude);
};

export const ButterflyMap = (props) => {
const {customFilters} = props;
const [position, setPosition] = React.useState(props.center);
Expand All @@ -58,8 +72,8 @@ export const ButterflyMap = (props) => {
const [typeOptions, setTypeOptions] = React.useState([]);
const [showAllTypes, setShowAllTypes] = React.useState(() => {
if (customFilters) {
for (const customFilter of customFilters){
if (customFilter.defaultValue === false){
for (const customFilter of customFilters) {
if (customFilter.defaultValue === false) {
return false;
}
}
Expand All @@ -68,21 +82,48 @@ export const ButterflyMap = (props) => {
});
const [showClosedRightNow, setShowClosedRightNow] = React.useState(true);
const [displayPointTypes, setDisplayPointTypes] = React.useState([]);
const [displayPoints, setDisplayPoints] = React.useState([]);
const [userPosition, setUserPosition] = React.useState(null);
const [positionRetry, setPositionRetry] = React.useState(false);
const [locationBlocked, setLocationBlocked] = React.useState(true);
const [centerMapDisabled, setCenterMapDisabled] = React.useState(true);
const [paginationPage, setPaginationPage] = React.useState(1);
const [hideMap, setHideMap] = React.useState(false);
const [hoursSet, setHoursSet] = React.useState(() => {
const [sidebarShowing, setSidebarShowing] = React.useState(false);
const [customFilterValues, setCustomFilterValues] = React.useState(customFilters ? customFilters.map((customFilter) => customFilter.defaultValue) : []);
const [hoursSet] = React.useState(() => {
for (const pointType of props.pointTypes) {
for (const point of pointType.points){
if (point.hours){
for (const point of pointType.points) {
if (point.hours) {
return true;
}
}
}
return false;
});
const [customFilterValues, setCustomFilterValues] = React.useState(customFilters ? customFilters.map((customFilter) => customFilter.defaultValue) : []);
const [viewport, setViewport] = React.useState({
...props.center,
zoom: props.zoom ?? 8,
width: 'fit',
height: props.height,
});

React.useEffect(() => {
const newPoints = [];
displayPointTypes.map((pointType) => {
pointType.points.map((point, index) => {
newPoints.push({...point, type: pointType.name, index: index});
});
});

if (newPoints.length !== displayPoints.length) {
setPaginationPage(1);
}

setDisplayPoints(newPoints.sort((a, b) => {
return calculateDistance(position, a.position) - calculateDistance(position, b.position);
}));
}, [displayPointTypes, position]);

const updateCustomFilterValue = (index, newVal) => {
const newCustomFilterValues = [...customFilterValues];
Expand All @@ -97,13 +138,6 @@ export const ButterflyMap = (props) => {
updateDisplayPointTypes(props.pointTypes, false, showClosedRightNow, newCustomFilterValues);
};

const [viewport, setViewport] = React.useState({
...props.center,
zoom: props.zoom ?? 8,
width: 'fit',
height: props.height,
});

const theme = {
reduceMotion: reduceMotion,
error: props.theme?.error ?? 'red',
Expand All @@ -116,6 +150,21 @@ export const ButterflyMap = (props) => {
popupBackgroundColor: props.theme?.popupBackgroundColor ?? 'white',
highlightOptionColor: props.theme?.highlightOptionColor ?? 'lightgray',
typePopupMinWidth: props.theme?.typePopupMinWidth ?? '15rem',
backgroundColor: props.theme?.backgroundColor ?? '#fff',
};

const handlePointBarMarkerClick = (e, point) => {
moveMapPosition(e, point.position);

// Browsers have issues rendering too many animations at once, resulting in a buggy sidebar entry animation
// if it's triggered at the same time as the map is moved.
// To avoid that we wait for 50ms before showing the sidebar.
window.setTimeout(() => setSidebarShowing(true), 50);
};

const handleMapMarkerClick = (position) => {
doMapMove(position);
window.setTimeout(() => setSidebarShowing(true), 50);
};

const updateDisplayPointTypes = (proposedTypes, updateTypeOptions = false, showClosed = showClosedRightNow, currentCustomFilterValues = customFilterValues) => {
Expand Down Expand Up @@ -289,25 +338,39 @@ export const ButterflyMap = (props) => {

React.useEffect(() => {
const updateUserPosition = () => {
try {
if (isSecureContext) {
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition((position) => {
setCenterMapDisabled(false);
setUserPosition({latitude: position.coords.latitude, longitude: position.coords.longitude});
});
return true;
if (window.localStorage.getItem('geolocationBlocked') !== 'true' && !locationBlocked) {
try {
if (isSecureContext) {
if ('geolocation' in navigator) {
navigator.geolocation.getCurrentPosition((position) => {
setCenterMapDisabled(false);
setLocationBlocked(false);
setUserPosition({latitude: position.coords.latitude, longitude: position.coords.longitude});
}, () => {
window.localStorage.setItem('geolocationBlocked', 'true');
setLocationBlocked(true);
});
return true;
} else {
console.info('Geolocation permission not given or feature not available.');
}
} else {
console.info('Geolocation permission not given or feature not available.');
console.info('Geolocation is only available in a secure context');
}
} else {
console.info('Geolocation is only available in a secure context');
} catch {
console.debug('Could not get geolocation. Are you using a somewhat modern browser?');
}
} catch {
console.debug('Could not get geolocation. Are you using a somewhat modern browser?');
} else {
setCenterMapDisabled(false);
setLocationBlocked(true);
}
return false;
};

window.addEventListener('scroll', () => {
setLocationBlocked(false);
}, {once: true})

let updateUserPositionInterval = false;
if (updateUserPosition()) {
updateUserPositionInterval = window.setInterval(updateUserPosition, 30000);
Expand All @@ -318,11 +381,16 @@ export const ButterflyMap = (props) => {
clearInterval(updateUserPositionInterval);
}
};
}, []);
}, [positionRetry, locationBlocked]);

const handleCenterMapClick = () => {
if (window.localStorage.getItem('geolocationBlocked') === 'true') {
window.localStorage.setItem('geolocationBlocked', 'false');
}
if (userPosition) {
doMapMove(userPosition, false);
} else {
setPositionRetry(!positionRetry);
}
};

Expand All @@ -347,25 +415,36 @@ export const ButterflyMap = (props) => {
customFilterValues={customFilterValues}
updateCustomFilterValue={updateCustomFilterValue}
/>
{!hideMap && <>
<ReactMapGL {...viewport}
onViewportChange={(newViewport) => setViewport(newViewport)}
mapStyle={props.tileServer}>
<Markers doMapMove={doMapMove} displayPointTypes={displayPointTypes}/>
<AttributionControl compact={true}/>
</ReactMapGL>
<CenterMapButton disabled={centerMapDisabled} onClick={handleCenterMapClick}>
{props.localStrings?.centerMap ?? 'Center map on current location'}
</CenterMapButton>
</>}
{!hideMap && <MapAndBarContainer>
<MapSidebar data-showing={sidebarShowing} height={props.height}>
{displayPoints.length > 0 &&
<SidebarContent userPosition={userPosition}
point={displayPoints[0]}
localStrings={props.localStrings}
setSidebarShowing={setSidebarShowing}/>}
</MapSidebar>
<MapContainer>
<ReactMapGL {...viewport}
onViewportChange={(newViewport) => setViewport(newViewport)}
mapStyle={props.tileServer}>
<Markers handleMapMarkerClick={handleMapMarkerClick} displayPointTypes={displayPointTypes}/>
<AttributionControl compact={true}/>
</ReactMapGL>
<CenterMapButton
title={locationBlocked ? props.localStrings?.location_permission_needed ?? 'In order to center the map, you have to allow this website to use your current position' : ''}
disabled={centerMapDisabled}
onClick={handleCenterMapClick}>
{props.localStrings?.centerMap ?? 'Center map on current location'}
</CenterMapButton>
</MapContainer>
</MapAndBarContainer>}
{typeOptions.length > 0 &&
<PointBar pointTypes={displayPointTypes}
position={position}
moveMapPosition={moveMapPosition}
userPosition={userPosition}
<PointBar userPosition={userPosition}
localStrings={props.localStrings}
page={paginationPage}
setPage={setPaginationPage}
displayPoints={displayPoints}
handlePointBarMarkerClick={handlePointBarMarkerClick}
/>}
</ThemeProvider>;
};
Expand Down Expand Up @@ -416,5 +495,6 @@ ButterflyMap.propTypes = {
popupBackgroundColor: PropTypes.string,
highlightOptionColor: PropTypes.string,
typePopupMinWidth: PropTypes.string,
backgroundColor: PropTypes.string,
}),
};
60 changes: 25 additions & 35 deletions assets/js/components/OpenHours.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ const HoursRow = (props) => {
return <Tr>
<Th scope="row">{title}</Th>
{typeof dayHours === 'boolean'
? dayHours === true ? <OpenDay>{localStrings?.allDay ?? 'All day'}</OpenDay> :
<ClosedDay>{localStrings?.closed ?? 'Closed'}</ClosedDay>
? dayHours === true ? <OpenDay className='special-day'>{localStrings?.allDay ?? 'All day'}</OpenDay> :
<ClosedDay className='special-day'>{localStrings?.closed ?? 'Closed'}</ClosedDay>
: <>
{!!closed
? <ClosedDay>
? <ClosedDay className='special-day'>
<DayHoursTd dayHours={dayHours} localStrings={localStrings}/>
</ClosedDay>
: <Td>
Expand Down Expand Up @@ -131,42 +131,32 @@ const OpenHours = (props) => {
const hour = now.getHours();
const minutes = now.getMinutes();

if (displayMore) {
return <>
<HoursTable>
<tbody>
{Object.entries(hours).map(([day, dayHours], index) => {
return <>
<HoursTable>
<tbody>
{displayMore
? Object.entries(hours).map(([day, dayHours], index) => {
if (typeof dayHours === 'object' || typeof dayHours === 'boolean') {
return <HoursRow key={index} title={localStrings?.[day] ?? day}
dayHours={dayHours} localStrings={localStrings}/>;
}
})}
</tbody>
</HoursTable>
<UntilWarning until={props.until} localStrings={localStrings}/>
{!!hours.text && <Small>{hours.text}</Small>}
<MoreOrLessButton onClick={() => setDisplayMore(false)}>
{localStrings?.showLess ?? 'Show less'}
</MoreOrLessButton>
</>;
} else {
return <>
<HoursTable>
<tbody>
<HoursRow title={localStrings?.today ?? 'Today'} dayHours={hours[weekdays[day]]}
localStrings={localStrings}
closed={!filterHours({hours: hours}, day, hour, minutes)}/>
<HoursRow title={localStrings?.tomorrow ?? 'Tomorrow'}
dayHours={hours[weekdays[day === 6 ? 0 : day + 1]]}
localStrings={localStrings}/>
</tbody>
</HoursTable>
<UntilWarning until={props.until} localStrings={localStrings}/>
<MoreOrLessButton onClick={() => setDisplayMore(true)}>
{localStrings?.showMore ?? 'Show more'}
</MoreOrLessButton>
</>;
}
})
: <>
<HoursRow title={localStrings?.today ?? 'Today'} dayHours={hours[weekdays[day]]}
localStrings={localStrings}
closed={!filterHours({hours: hours}, day, hour, minutes)}/>
<HoursRow title={localStrings?.tomorrow ?? 'Tomorrow'}
dayHours={hours[weekdays[day === 6 ? 0 : day + 1]]}
localStrings={localStrings}/>
</>}
</tbody>
</HoursTable>
<UntilWarning until={props.until} localStrings={localStrings}/>
{!!hours.text && <Small>{hours.text}</Small>}
<MoreOrLessButton onClick={() => setDisplayMore(!displayMore)}>
{displayMore ? localStrings?.showLess ?? 'Show less' : localStrings?.showMore ?? 'Show more'}
</MoreOrLessButton>
</>;
};

// Hours has to be an array with the index 0 containing hours for sunday and counting up from there.
Expand Down
Loading

0 comments on commit a62c5ec

Please sign in to comment.