Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 1622/activist portal events map #1652

Open
wants to merge 21 commits into
base: epic-1615/activist-portal-base
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/features/events/components/EventMap/LocationDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { PropsWithChildren } from 'react';
import { Box, useTheme } from '@mui/system';
import { Button, Drawer, useMediaQuery } from '@mui/material';

const LocationDrawer = ({
onClose,
children,
}: PropsWithChildren<{ onClose: () => void }>) => {
const theme = useTheme();
const isDesktop = useMediaQuery(theme.breakpoints.up('md'));
return (
<Drawer
anchor={isDesktop ? 'right' : 'bottom'}
onClose={onClose}
open={Boolean(children)}
>
<Button onClick={onClose}>Close</Button>
<Box onClick={onClose} padding={2} role="presentation">
{children}
</Box>
</Drawer>
);
};

export default LocationDrawer;
42 changes: 42 additions & 0 deletions src/features/events/components/EventMap/groupEventsByLocation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { EventActivity } from 'features/campaigns/types';
import { LocationWithData } from 'zui/ZUIMap';

const groupEventsByLocation = (
events: EventActivity[]
): LocationWithData<EventActivity>[] => {
const locationsWithEvents = events.reduce(
(
acc: {
[key: string]: LocationWithData<EventActivity>;
},
event
) => {
const { location } = event.data;
if (!location) {
return acc;
}
// If no location yet
if (!acc[location.id]) {
return {
...acc,
[location.id]: {
...location,
data: [event],
},
};
} else {
return {
...acc,
[location.id]: {
...acc[location.id],
data: [...acc[location.id].data, event],
},
};
}
},
{}
);
return Object.values(locationsWithEvents);
};

export default groupEventsByLocation;
49 changes: 49 additions & 0 deletions src/features/events/components/EventMap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Box } from '@mui/system';
import { LatLngLiteral } from 'leaflet';
import { useState } from 'react';

import { EventActivity } from 'features/campaigns/types';
import EventSignUpList from '../EventSignUpList';
import groupEventsByLocation from './groupEventsByLocation';
import LocationDrawer from './LocationDrawer';
import ZUIMap, { LocationWithData } from 'zui/ZUIMap';

const EventMap = ({
center,
events,
}: {
center?: LatLngLiteral;
events: EventActivity[];
}) => {
const [selectedLocation, setSelectedLocation] =
useState<LocationWithData<EventActivity>>();

const locationsWithEvents = groupEventsByLocation(events);

return (
<Box height="100vh" position="relative">
<ZUIMap
center={center}
locations={locationsWithEvents}
onClickLocation={(location) => {
setSelectedLocation(location);
}}
selectedLocation={selectedLocation}
/>

<LocationDrawer
onClose={() => {
setSelectedLocation(undefined);
}}
>
{selectedLocation && (
<EventSignUpList
events={selectedLocation?.data.map(({ data: event }) => event)}
/>
)}
</LocationDrawer>
</Box>
);
};

export default EventMap;
19 changes: 18 additions & 1 deletion src/pages/o/[orgId]/map/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import dynamic from 'next/dynamic';
import { FC } from 'react';
import { scaffold } from 'utils/next';
import useEventActivities from 'features/campaigns/hooks/useEventActivities';
import { ACTIVITIES, EventActivity } from 'features/campaigns/types';

const EventsMap = dynamic(() => import('features/events/components/EventMap'), {
ssr: false,
});

const scaffoldOptions = {
allowNonOfficials: true,
Expand All @@ -21,7 +28,17 @@ type PageProps = {
};

const Page: FC<PageProps> = ({ orgId }) => {
return <h1>Map page for org {orgId}</h1>;
const { data: activities } = useEventActivities(parseInt(orgId));

if (activities && activities.length > 0) {
// Get event activities
const events = activities.filter(
(activity) => activity.kind === ACTIVITIES.EVENT
) as EventActivity[];

return <EventsMap events={events} />;
}
return null;
};

export default Page;
59 changes: 59 additions & 0 deletions src/zui/ZUIMap/BasicMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { FC } from 'react';
import { makeStyles } from '@mui/styles';
import { Box, Theme, Typography } from '@mui/material';

interface StyleProps {
color: string;
}

const useStyles = makeStyles<Theme, StyleProps>(() => ({
number: {
bottom: 3,
color: ({ color }) => color,
left: 2,
position: 'absolute',
right: 5,
textAlign: 'center',
},
parent: {
height: 41,
position: 'relative',
width: 28,
},
}));

interface BasicMarkerProps {
color: string;
events: number;
}

const BasicMarker: FC<BasicMarkerProps> = ({ color, events }) => {
const classes = useStyles({ color });
return (
<Box className={classes.parent}>
{events > 0 && (
<Box className={classes.number}>
<Typography>{events > 9 ? '9+' : events}</Typography>
</Box>
)}
<svg fill="none" height="40" viewBox="0 0 31 40" width="27">
<path
d="M14 38.479C13.6358 38.0533 13.1535 37.4795
12.589 36.7839C11.2893 35.1826 9.55816 32.9411
7.82896 30.3782C6.09785 27.8124 4.38106 24.9426
3.1001 22.0833C1.81327 19.211 1 16.4227 1 14C1
6.81228 6.81228 1 14 1C21.1877 1 27 6.81228 27 14C27
16.4227 26.1867 19.211 24.8999 22.0833C23.6189 24.9426
21.9022 27.8124 20.171 30.3782C18.4418 32.9411 16.7107
35.1826 15.411 36.7839C14.8465 37.4795 14.3642
38.0533 14 38.479Z"
fill="white"
stroke="#ED1C55"
strokeWidth="2"
/>
</svg>
</Box>
);
};

export default BasicMarker;
33 changes: 33 additions & 0 deletions src/zui/ZUIMap/SelectedMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { FC } from 'react';

const SelectedMarker: FC = () => {
return (
<svg fill="none" height="50" viewBox="0 0 44 63" width="40">
<path
d="M22 61L21.6289 61.3351L22 61.7459L22.3711
61.3351L22 61ZM22 61C22.3711 61.3351 22.3712
61.335 22.3714 61.3348L22.3722 61.3338L22.3753
61.3304L22.3872 61.3172L22.4331 61.266C22.4734
61.2208 22.533 61.1539 22.6106 61.0661C22.7657
60.8905 22.9926 60.6315 23.2811 60.2966C23.8582
59.6268 24.6817 58.6533 25.6695 57.4362C27.6447
55.0025 30.2791 51.5919 32.9145 47.6859C35.5489
43.7813 38.1905 39.3724 40.1751 34.9427C42.1566
30.5195 43.5 26.0387 43.5 22C43.5 10.1139 33.8861
0.5 22 0.5C10.1139 0.5 0.5 10.1139 0.5 22C0.5
26.0387 1.84336 30.5195 3.82495 34.9427C5.80947
39.3724 8.45108 43.7813 11.0855 47.6859C13.7209
51.5919 16.3553 55.0025 18.3305 57.4362C19.3183
58.6533 20.1418 59.6268 20.7189 60.2966C21.0074
60.6315 21.2343 60.8905 21.3894 61.0661C21.467
61.1539 21.5266 61.2208 21.5669 61.266L21.6128
61.3172L21.6247 61.3304L21.6278 61.3338L21.6286
61.3348C21.6288 61.335 21.6289 61.3351 22 61Z"
fill="#ED1C55"
stroke="white"
/>
</svg>
);
};

export default SelectedMarker;
107 changes: 107 additions & 0 deletions src/zui/ZUIMap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import 'leaflet/dist/leaflet.css';
import { renderToStaticMarkup } from 'react-dom/server';
import { useTheme } from '@mui/material';
import {
divIcon,
latLngBounds,
LatLngLiteral,
Map as LeafletMap,
} from 'leaflet';
import { MapContainer, Marker, TileLayer, useMap } from 'react-leaflet';

import BasicMarker from './BasicMarker';
import SelectedMarker from './SelectedMarker';
import { ZetkinLocation } from 'utils/types/zetkin';

const MapWrapper = ({
children,
}: {
children: (map: LeafletMap) => JSX.Element;
}) => {
const map = useMap();
return children(map);
};

export type LocationWithData<T = unknown> = ZetkinLocation & { data: T[] };

const ZUIMap = <T extends unknown>({
center,
locations,
onClickLocation,
selectedLocation,
}: {
center?: LatLngLiteral;
locations: LocationWithData<T>[];
onClickLocation: (location: LocationWithData<T>) => void;
selectedLocation?: LocationWithData<T>;
}) => {
const theme = useTheme();

return (
<MapContainer
bounds={
locations.length
? latLngBounds(
locations.map((location) => ({
lat: location.lat,
lng: location.lng,
}))
)
: [
[75, -170],
[-60, 180],
]
}
style={{ height: '100%', width: '100%' }}
>
<MapWrapper>
{(map) => {
// Set map center if set externally
if (center) {
map.setView(center, 13);
}
return (
<>
<TileLayer
attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>

{locations.map((location) => {
return (
<Marker
key={location.id}
eventHandlers={{
click: () => {
onClickLocation(location);
},
}}
icon={
selectedLocation?.id === location.id
? divIcon({
className: '',
html: renderToStaticMarkup(<SelectedMarker />),
})
: divIcon({
className: '',
html: renderToStaticMarkup(
<BasicMarker
color={theme.palette.primary.main}
events={location.data.length}
/>
),
})
}
position={[location.lat, location.lng]}
/>
);
})}
</>
);
}}
</MapWrapper>
</MapContainer>
);
};

export default ZUIMap;
Loading