From e824e2929434cf1644456f9acddd1df966c7da3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20J=C3=B6nsson?= Date: Sat, 11 Nov 2023 12:35:34 +0100 Subject: [PATCH 1/9] Render all data and map for event information page --- src/pages/o/[orgId]/events/[eventId]/Map.tsx | 57 +++++++++++++++++++ .../o/[orgId]/events/[eventId]/index.tsx | 51 ++++++++++++++++- 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/pages/o/[orgId]/events/[eventId]/Map.tsx diff --git a/src/pages/o/[orgId]/events/[eventId]/Map.tsx b/src/pages/o/[orgId]/events/[eventId]/Map.tsx new file mode 100644 index 0000000000..865da70393 --- /dev/null +++ b/src/pages/o/[orgId]/events/[eventId]/Map.tsx @@ -0,0 +1,57 @@ +import 'leaflet/dist/leaflet.css'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { divIcon, latLngBounds, Map as MapType } from 'leaflet'; +import { MapContainer, Marker, TileLayer, useMap } from 'react-leaflet'; + +import SelectedMarker from 'features/events/components/LocationModal/SelectedMarker'; + +const MapWrapper = ({ + children, +}: { + children: (map: MapType) => JSX.Element; +}) => { + const map = useMap(); + return children(map); +}; + +type MapProps = { + location: { + id: number; + lat: number; + lng: number; + }; +}; + +const Map = ({ location }: MapProps) => { + return ( + + + {(map) => { + map.setView({ lat: location.lat, lng: location.lng }, 17); + + return ( + <> + + ), + })} + position={[location.lat, location.lng]} + /> + + ); + }} + + + ); +}; + +export default Map; diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index de17609a0b..22aaa4e0b3 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -1,5 +1,14 @@ +import 'leaflet/dist/leaflet.css'; +import dynamic from 'next/dynamic'; import { FC } from 'react'; +import NextLink from 'next/link'; +import { Avatar, Box, Link, Typography } from '@mui/material'; + import { scaffold } from 'utils/next'; +import useEvent from 'features/events/hooks/useEvent'; +import useServerSide from 'core/useServerSide'; +import ZUIDateTime from 'zui/ZUIDateTime'; +import ZUIFuture from 'zui/ZUIFuture'; const scaffoldOptions = { allowNonOfficials: true, @@ -22,11 +31,47 @@ type PageProps = { orgId: string; }; +const Map = dynamic(() => import('./Map'), { ssr: false }); + const Page: FC = ({ orgId, eventId }) => { + const eventFuture = useEvent(parseInt(orgId, 10), parseInt(eventId, 10)); + const onServer = useServerSide(); + + if (onServer) { + return null; + } + return ( -

- Page for org {orgId}, event {eventId} -

+ + {(event) => { + const location = event.location; + return ( + + + {event.organization.title} + {event.title} + {event.info_text} + + {location && {location.title}} + {event.campaign && ( + + + {event.campaign.title} + + + )} + {location && ( + + + + )} + + ); + }} + ); }; From ce1ff46b06d9982e4a6ebcaf27a1940306cf68be Mon Sep 17 00:00:00 2001 From: ziggi Date: Sat, 11 Nov 2023 15:09:22 +0100 Subject: [PATCH 2/9] Move Map component to Event feature components folder. --- .../events/components/ActivistEventPage}/Map.tsx | 0 src/pages/o/[orgId]/events/[eventId]/index.tsx | 5 ++++- 2 files changed, 4 insertions(+), 1 deletion(-) rename src/{pages/o/[orgId]/events/[eventId] => features/events/components/ActivistEventPage}/Map.tsx (100%) diff --git a/src/pages/o/[orgId]/events/[eventId]/Map.tsx b/src/features/events/components/ActivistEventPage/Map.tsx similarity index 100% rename from src/pages/o/[orgId]/events/[eventId]/Map.tsx rename to src/features/events/components/ActivistEventPage/Map.tsx diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index 22aaa4e0b3..763a4d54e5 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -31,7 +31,10 @@ type PageProps = { orgId: string; }; -const Map = dynamic(() => import('./Map'), { ssr: false }); +const Map = dynamic( + () => import('features/events/components/ActivistEventPage/Map'), + { ssr: false } +); const Page: FC = ({ orgId, eventId }) => { const eventFuture = useEvent(parseInt(orgId, 10), parseInt(eventId, 10)); From 29c4c86e47d17fe0299e6f362ad48aed8f711162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20J=C3=B6nsson?= Date: Sat, 11 Nov 2023 18:59:18 +0100 Subject: [PATCH 3/9] Add hook for actions in order to sign up for events, as well as signup state --- src/features/events/hooks/useEventBookings.ts | 22 ++++ src/features/events/hooks/useEventSignup.ts | 108 ++++++++++++++++++ src/features/events/hooks/useEventSignups.ts | 18 +++ src/features/events/store.ts | 46 ++++++++ src/pages/api/[...path].ts | 6 +- .../o/[orgId]/events/[eventId]/index.tsx | 19 ++- src/utils/next.ts | 3 +- 7 files changed, 216 insertions(+), 6 deletions(-) create mode 100644 src/features/events/hooks/useEventBookings.ts create mode 100644 src/features/events/hooks/useEventSignup.ts create mode 100644 src/features/events/hooks/useEventSignups.ts diff --git a/src/features/events/hooks/useEventBookings.ts b/src/features/events/hooks/useEventBookings.ts new file mode 100644 index 0000000000..809eb53e48 --- /dev/null +++ b/src/features/events/hooks/useEventBookings.ts @@ -0,0 +1,22 @@ +import { IFuture } from 'core/caching/futures'; +import { loadListIfNecessary } from 'core/caching/cacheUtils'; +import { ZetkinEvent } from 'utils/types/zetkin'; +import { bookingsLoad, bookingsLoaded } from 'features/events/store'; +import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks'; + +export default function useEventBookings(): IFuture { + const apiClient = useApiClient(); + const dispatch = useAppDispatch(); + const bookingsList = useAppSelector((state) => state.events.bookingsList); + + return loadListIfNecessary(bookingsList, dispatch, { + actionOnLoad: () => dispatch(bookingsLoad()), + actionOnSuccess: (data) => dispatch(bookingsLoaded(data)), + loader: () => + apiClient.get( + `/api/users/me/actions?filter=${encodeURIComponent( + 'status!=cancelled' + )}` + ), + }); +} diff --git a/src/features/events/hooks/useEventSignup.ts b/src/features/events/hooks/useEventSignup.ts new file mode 100644 index 0000000000..5a871570a5 --- /dev/null +++ b/src/features/events/hooks/useEventSignup.ts @@ -0,0 +1,108 @@ +import useEventBookings from './useEventBookings'; +import useEventSignups from './useEventSignups'; +import useMemberships from 'features/campaigns/hooks/useMemberships'; +import { + ErrorFuture, + IFuture, + LoadingFuture, + ResolvedFuture, +} from 'core/caching/futures'; +import { signupAdd, signupAdded, signupRemove, signupRemoved } from '../store'; +import { useApiClient, useAppDispatch } from 'core/hooks'; +import { + ZetkinEvent, + ZetkinEventResponse, + ZetkinMembership, +} from 'utils/types/zetkin'; + +type EventResponseState = + | 'notSignedUp' + | 'responded' + | 'participant' + | 'notInOrgYet'; + +function eventResponseState( + eventId: number, + bookings: ZetkinEvent[], + membership: ZetkinMembership, + signups: ZetkinEventResponse[] +): EventResponseState { + if (!membership) { + return 'notInOrgYet'; + } + if (bookings.some((b) => b.id == eventId)) { + return 'participant'; + } + if (signups.some((b) => b.action_id == eventId)) { + return 'responded'; + } + return 'notSignedUp'; +} + +type UseEventSignupReturn = { + myResponseState: EventResponseState; + signup: () => Promise; + undoSignup: () => Promise; +}; + +export default function useEventSignup( + orgId: number, + eventId: number +): IFuture { + const apiClient = useApiClient(); + const dispatch = useAppDispatch(); + const bookingsFuture = useEventBookings(); + const membershipsFuture = useMemberships(); + const signupsFuture = useEventSignups(); + + const allFutures: IFuture[] = [ + bookingsFuture, + membershipsFuture, + signupsFuture, + ]; + + if (allFutures.some((f) => f.isLoading)) { + return new LoadingFuture(); + } + if (allFutures.some((f) => f.error)) { + return new ErrorFuture('Error loading bookings'); + } + + // TODO: handle "missing" user + const membership = membershipsFuture.data!.find( + (m) => m.organization.id == orgId + ); + + const signup = async () => { + dispatch(signupAdd()); + await apiClient.put( + `/api/orgs/${orgId}/actions/${eventId}/responses/${membership?.profile.id}` + ); + dispatch( + signupAdded({ + action_id: eventId, + id: membership!.profile.id, + person: membership!.profile, + response_date: new Date().toUTCString(), + }) + ); + }; + const undoSignup = async () => { + dispatch(signupRemove()); + await apiClient.delete( + `/api/orgs/${orgId}/actions/${eventId}/responses/${membership?.profile.id}` + ); + dispatch(signupRemoved(eventId)); + }; + + return new ResolvedFuture({ + myResponseState: eventResponseState( + eventId, + bookingsFuture.data!, + membership!, + signupsFuture.data! + ), + signup, + undoSignup, + }); +} diff --git a/src/features/events/hooks/useEventSignups.ts b/src/features/events/hooks/useEventSignups.ts new file mode 100644 index 0000000000..a16ff5c1bf --- /dev/null +++ b/src/features/events/hooks/useEventSignups.ts @@ -0,0 +1,18 @@ +import { IFuture } from 'core/caching/futures'; +import { loadListIfNecessary } from 'core/caching/cacheUtils'; +import { ZetkinEventResponse } from 'utils/types/zetkin'; +import { signupsLoad, signupsLoaded } from 'features/events/store'; +import { useApiClient, useAppDispatch, useAppSelector } from 'core/hooks'; + +export default function useEventSignups(): IFuture { + const apiClient = useApiClient(); + const dispatch = useAppDispatch(); + const signupsList = useAppSelector((state) => state.events.signupsList); + + return loadListIfNecessary(signupsList, dispatch, { + actionOnLoad: () => dispatch(signupsLoad()), + actionOnSuccess: (data) => dispatch(signupsLoaded(data)), + loader: () => + apiClient.get(`/api/users/me/action_responses`), + }); +} diff --git a/src/features/events/store.ts b/src/features/events/store.ts index 1dabcfe17b..131b65c4de 100644 --- a/src/features/events/store.ts +++ b/src/features/events/store.ts @@ -49,6 +49,7 @@ export type FilterCategoryType = | 'selectedTypes'; export interface EventsStoreSlice { + bookingsList: RemoteList; eventList: RemoteList; eventsByCampaignId: Record>; eventsByDate: Record>; @@ -62,11 +63,13 @@ export interface EventsStoreSlice { participantsByEventId: Record>; respondentsByEventId: Record>; selectedEventIds: number[]; + signupsList: RemoteList; statsByEventId: Record>; typeList: RemoteList; } const initialState: EventsStoreSlice = { + bookingsList: remoteList(), eventList: remoteList(), eventsByCampaignId: {}, eventsByDate: {}, @@ -80,6 +83,7 @@ const initialState: EventsStoreSlice = { participantsByEventId: {}, respondentsByEventId: {}, selectedEventIds: [], + signupsList: remoteList(), statsByEventId: {}, typeList: remoteList(), }; @@ -88,6 +92,13 @@ const eventsSlice = createSlice({ initialState, name: 'events', reducers: { + bookingsLoad: (state) => { + state.bookingsList.isLoading = true; + }, + bookingsLoaded: (state, action: PayloadAction) => { + state.bookingsList = remoteList(action.payload); + state.bookingsList.loaded = new Date().toISOString(); + }, campaignEventsLoad: (state, action: PayloadAction) => { const id = action.payload; state.eventsByCampaignId[id] = remoteList(); @@ -515,6 +526,33 @@ const eventsSlice = createSlice({ state.respondentsByEventId[eventId] = remoteList(respondents); state.respondentsByEventId[eventId].loaded = new Date().toISOString(); }, + signupAdd: (state) => { + state.signupsList.isLoading = true; + }, + signupAdded: (state, action: PayloadAction) => { + const eventResponse = action.payload; + state.signupsList.isLoading = false; + state.signupsList.items.push( + remoteItem(eventResponse.action_id, { data: eventResponse }) + ); + }, + signupRemove: (state) => { + state.signupsList.isLoading = true; + }, + signupRemoved: (state, action: PayloadAction) => { + const eventId = action.payload; + state.signupsList.isLoading = false; + state.signupsList.items = state.signupsList.items.filter( + (s) => s.data?.action_id != eventId + ); + }, + signupsLoad: (state) => { + state.signupsList.isLoading = true; + }, + signupsLoaded: (state, action: PayloadAction) => { + state.signupsList = remoteList(action.payload); + state.signupsList.loaded = new Date().toISOString(); + }, statsLoad: (state, action: PayloadAction) => { const eventId = action.payload; state.statsByEventId[eventId] = remoteItem(eventId); @@ -573,6 +611,8 @@ const eventsSlice = createSlice({ export default eventsSlice; export const { + bookingsLoad, + bookingsLoaded, campaignEventsLoad, campaignEventsLoaded, eventCreate, @@ -609,6 +649,12 @@ export const { resetSelection, respondentsLoad, respondentsLoaded, + signupAdd, + signupAdded, + signupRemove, + signupRemoved, + signupsLoad, + signupsLoaded, statsLoad, statsLoaded, typeAdd, diff --git a/src/pages/api/[...path].ts b/src/pages/api/[...path].ts index 2ebadd286f..9535cadefe 100644 --- a/src/pages/api/[...path].ts +++ b/src/pages/api/[...path].ts @@ -4,7 +4,6 @@ import { applySession } from 'next-session'; import type { NextApiRequest, NextApiResponse } from 'next'; import { AppSession } from 'utils/types'; -import getFilters from 'utils/getFilters'; import { stringToBool } from 'utils/stringUtils'; import { ZetkinZResource, ZetkinZResult } from 'utils/types/sdk'; @@ -20,9 +19,8 @@ interface HttpVerbMethod { const HTTP_VERBS_TO_ZETKIN_METHODS: Record = { DELETE: (resource: ZetkinZResource) => resource.del(), - GET: (resource: ZetkinZResource, req: NextApiRequest) => { - const filters = getFilters(req); - return resource.get(null, null, filters); + GET: (resource: ZetkinZResource) => { + return resource.get(); }, PATCH: (resource: ZetkinZResource, req: NextApiRequestWithSession) => resource.patch(req.body), diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index 763a4d54e5..133e88b6d4 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -2,15 +2,17 @@ import 'leaflet/dist/leaflet.css'; import dynamic from 'next/dynamic'; import { FC } from 'react'; import NextLink from 'next/link'; -import { Avatar, Box, Link, Typography } from '@mui/material'; +import { Avatar, Box, Button, Link, Typography } from '@mui/material'; import { scaffold } from 'utils/next'; import useEvent from 'features/events/hooks/useEvent'; +import useEventSignup from 'features/events/hooks/useEventSignup'; import useServerSide from 'core/useServerSide'; import ZUIDateTime from 'zui/ZUIDateTime'; import ZUIFuture from 'zui/ZUIFuture'; const scaffoldOptions = { + allowNonMembers: true, allowNonOfficials: true, authLevelRequired: 1, }; @@ -39,6 +41,10 @@ const Map = dynamic( const Page: FC = ({ orgId, eventId }) => { const eventFuture = useEvent(parseInt(orgId, 10), parseInt(eventId, 10)); const onServer = useServerSide(); + const eventSignupFuture = useEventSignup( + parseInt(orgId, 10), + parseInt(eventId, 10) + ); if (onServer) { return null; @@ -66,6 +72,17 @@ const Page: FC = ({ orgId, eventId }) => { )} + + {({ myResponseState, signup, undoSignup }) => + myResponseState == 'notSignedUp' ? ( + + ) : myResponseState == 'responded' ? ( + + ) : ( + {"You're signed up!"} + ) + } + {location && ( diff --git a/src/utils/next.ts b/src/utils/next.ts index db62bd8b73..7959d8f612 100644 --- a/src/utils/next.ts +++ b/src/utils/next.ts @@ -49,6 +49,7 @@ interface ResultWithProps { interface ScaffoldOptions { // Level can be 1 (simple sign-in) or 2 (two-factor authentication) authLevelRequired?: number; + allowNonMembers?: boolean; allowNonOfficials?: boolean; localeScope?: string[]; } @@ -155,7 +156,7 @@ export const scaffold = if (!ctx.user?.is_superuser) { //if the org is in your memberships, come in //if not, more checks - if (!hasOrg(reqWithSession, orgId)) { + if (!options?.allowNonMembers && !hasOrg(reqWithSession, orgId)) { //fetch your orgs again to see if they've been updated try { const allowNonOfficials = !!options?.allowNonOfficials; From 381b75c80899d24363d82ea27d80f40e878a709f Mon Sep 17 00:00:00 2001 From: ziggi Date: Sat, 11 Nov 2023 19:20:35 +0100 Subject: [PATCH 4/9] Add styling, localisation and change constanst names to make more sense. --- src/features/events/hooks/useEventSignup.ts | 10 +-- src/features/events/l10n/messageIds.ts | 9 +++ .../o/[orgId]/events/[eventId]/index.tsx | 75 +++++++++++++------ 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/features/events/hooks/useEventSignup.ts b/src/features/events/hooks/useEventSignup.ts index 5a871570a5..b7adb5e4e6 100644 --- a/src/features/events/hooks/useEventSignup.ts +++ b/src/features/events/hooks/useEventSignup.ts @@ -15,11 +15,7 @@ import { ZetkinMembership, } from 'utils/types/zetkin'; -type EventResponseState = - | 'notSignedUp' - | 'responded' - | 'participant' - | 'notInOrgYet'; +type EventResponseState = 'notSignedUp' | 'signedUp' | 'booked' | 'notInOrgYet'; function eventResponseState( eventId: number, @@ -31,10 +27,10 @@ function eventResponseState( return 'notInOrgYet'; } if (bookings.some((b) => b.id == eventId)) { - return 'participant'; + return 'booked'; } if (signups.some((b) => b.action_id == eventId)) { - return 'responded'; + return 'signedUp'; } return 'notSignedUp'; } diff --git a/src/features/events/l10n/messageIds.ts b/src/features/events/l10n/messageIds.ts index 8a5efb80e0..94d73509c4 100644 --- a/src/features/events/l10n/messageIds.ts +++ b/src/features/events/l10n/messageIds.ts @@ -1,6 +1,15 @@ import { m, makeMessages } from 'core/i18n'; export default makeMessages('feat.events', { + activistPortal: { + bookedMessage: m( + 'You are booked! If you want to cancel, reach out to contact person' + ), + loadingButton: m('Loading...'), + notInOrgMessage: m('You are not member of this org yet'), + signupButton: m('Sign up'), + undoSignupButton: m('Undo signup'), + }, addPerson: { addButton: m('Add person'), addPlaceholder: m('Start typing to add participant'), diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index 133e88b6d4..e1b08a6187 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -1,15 +1,17 @@ import 'leaflet/dist/leaflet.css'; import dynamic from 'next/dynamic'; import { FC } from 'react'; -import NextLink from 'next/link'; -import { Avatar, Box, Button, Link, Typography } from '@mui/material'; +import { Avatar, Box, Button, Typography } from '@mui/material'; +import messageIds from 'features/events/l10n/messageIds'; +import { Msg } from 'core/i18n'; import { scaffold } from 'utils/next'; import useEvent from 'features/events/hooks/useEvent'; import useEventSignup from 'features/events/hooks/useEventSignup'; import useServerSide from 'core/useServerSide'; import ZUIDateTime from 'zui/ZUIDateTime'; import ZUIFuture from 'zui/ZUIFuture'; +import { BeachAccess, CalendarToday, Place } from '@mui/icons-material'; const scaffoldOptions = { allowNonMembers: true, @@ -55,31 +57,58 @@ const Page: FC = ({ orgId, eventId }) => { {(event) => { const location = event.location; return ( - - - {event.organization.title} - {event.title} + + {event.title} + + + + {event.organization.title} + + + + {event.activity && ( + + + {event.activity.title} + + )} + + + + + {location && ( + + + {location.title} + + )} + {event.info_text} - - {location && {location.title}} - {event.campaign && ( - - - {event.campaign.title} - - - )} - + + + + } + > {({ myResponseState, signup, undoSignup }) => myResponseState == 'notSignedUp' ? ( - - ) : myResponseState == 'responded' ? ( - + + ) : myResponseState == 'signedUp' ? ( + + ) : myResponseState == 'booked' ? ( + + + ) : ( - {"You're signed up!"} + + + ) } From 38a020e18e6851b170d5cb27cb71f7addf258e7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20J=C3=B6nsson?= Date: Sun, 12 Nov 2023 10:50:07 +0100 Subject: [PATCH 5/9] Add title to event page, and make map expandable in the event page --- .../components/ActivistEventPage/Map.tsx | 24 ++- .../LocationModal/SelectedMarker.tsx | 6 +- src/features/events/l10n/messageIds.ts | 1 + .../o/[orgId]/events/[eventId]/index.tsx | 203 ++++++++++++------ 4 files changed, 161 insertions(+), 73 deletions(-) diff --git a/src/features/events/components/ActivistEventPage/Map.tsx b/src/features/events/components/ActivistEventPage/Map.tsx index 865da70393..65511b4ff6 100644 --- a/src/features/events/components/ActivistEventPage/Map.tsx +++ b/src/features/events/components/ActivistEventPage/Map.tsx @@ -1,4 +1,5 @@ import 'leaflet/dist/leaflet.css'; +import { CSSProperties } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { divIcon, latLngBounds, Map as MapType } from 'leaflet'; import { MapContainer, Marker, TileLayer, useMap } from 'react-leaflet'; @@ -15,22 +16,32 @@ const MapWrapper = ({ }; type MapProps = { + interactive: boolean; location: { id: number; lat: number; lng: number; }; + style: CSSProperties; + zoomLevel: number; }; -const Map = ({ location }: MapProps) => { +const Map = ({ interactive, location, style, zoomLevel }: MapProps) => { return ( {(map) => { - map.setView({ lat: location.lat, lng: location.lng }, 17); + map.setView({ lat: location.lat, lng: location.lng }, zoomLevel); return ( <> @@ -42,7 +53,12 @@ const Map = ({ location }: MapProps) => { key={location.id} icon={divIcon({ className: '', - html: renderToStaticMarkup(), + html: renderToStaticMarkup( + + ), + iconAnchor: [0, 0], })} position={[location.lat, location.lng]} /> diff --git a/src/features/events/components/LocationModal/SelectedMarker.tsx b/src/features/events/components/LocationModal/SelectedMarker.tsx index 9142524c0b..ad3ce22063 100644 --- a/src/features/events/components/LocationModal/SelectedMarker.tsx +++ b/src/features/events/components/LocationModal/SelectedMarker.tsx @@ -1,8 +1,8 @@ -import { FC } from 'react'; +import { CSSProperties, FC } from 'react'; -const SelectedMarker: FC = () => { +const SelectedMarker: FC<{ style?: CSSProperties }> = ({ style }) => { return ( - + - {event.title} - - - - {event.organization.title} - - - - {event.activity && ( + <> + + + <Msg id={messageIds.activistPortal.loadingTitle} /> + + + + {(event) => { + const location = event.location; + return ( + <> + + {event.title} + + + {event.title} - - {event.activity.title} + + + {event.organization.title} + - )} - - - - - {location && ( - - - {location.title} + + {event.activity && ( + + + {event.activity.title} + + )} + + + + + {location && ( + + + + {location.title} + + setShowBigMap(true)} + underline="hover" + > + Show on map + + setShowBigMap(false)} + open={showBigMap} + > + + + + + + )} - )} - - {event.info_text} - - - - } - > - {({ myResponseState, signup, undoSignup }) => - myResponseState == 'notSignedUp' ? ( - - ) : myResponseState == 'signedUp' ? ( - - ) : myResponseState == 'booked' ? ( - - - - ) : ( - - - - ) - } - - {location && ( - - + {event.info_text} + {location && ( + + + + )} - )} - - ); - }} - + + + + + + } + > + {({ myResponseState, signup, undoSignup }) => + myResponseState == 'notSignedUp' ? ( + + ) : myResponseState == 'signedUp' ? ( + + ) : myResponseState == 'booked' ? ( + + + + ) : ( + + + + ) + } + + + + + ); + }} + + ); }; From 84d67c6ba7a4558eaff093ae7cfd8b8602a36ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20J=C3=B6nsson?= Date: Sun, 12 Nov 2023 11:24:10 +0100 Subject: [PATCH 6/9] Improve signup-button behaviour in ecent page, as well as layout --- src/features/events/l10n/messageIds.ts | 2 + .../o/[orgId]/events/[eventId]/index.tsx | 48 ++++++++++++------- 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/features/events/l10n/messageIds.ts b/src/features/events/l10n/messageIds.ts index 58a83175d6..9b67dff06f 100644 --- a/src/features/events/l10n/messageIds.ts +++ b/src/features/events/l10n/messageIds.ts @@ -5,9 +5,11 @@ export default makeMessages('feat.events', { bookedMessage: m( 'You are booked! If you want to cancel, reach out to contact person' ), + joinOrgButton: m('Join organization'), loadingButton: m('Loading...'), loadingTitle: m('Loading event...'), notInOrgMessage: m('You are not member of this org yet'), + signedUp: m('Signed up!'), signupButton: m('Sign up'), undoSignupButton: m('Undo signup'), }, diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index 9195926ca2..de7e444ca6 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -1,6 +1,7 @@ import 'leaflet/dist/leaflet.css'; import dynamic from 'next/dynamic'; import Head from 'next/head'; +import NextLink from 'next/link'; import { Avatar, Box, @@ -20,7 +21,7 @@ import useEventSignup from 'features/events/hooks/useEventSignup'; import useServerSide from 'core/useServerSide'; import ZUIDateTime from 'zui/ZUIDateTime'; import ZUIFuture from 'zui/ZUIFuture'; -import { BeachAccess, CalendarToday, Place } from '@mui/icons-material'; +import { BeachAccess, CalendarToday, Done, Place } from '@mui/icons-material'; const scaffoldOptions = { allowNonMembers: true, @@ -77,19 +78,15 @@ const Page: FC = ({ orgId, eventId }) => { {event.title} - + {event.title} - - + + {event.organization.title} - + {event.activity && ( @@ -154,7 +151,7 @@ const Page: FC = ({ orgId, eventId }) => { )} - + = ({ orgId, eventId }) => { ) : myResponseState == 'signedUp' ? ( - ) : myResponseState == 'booked' ? ( - - - + <> + + + + + ) : ( - - - + <> + + + + + + + ) } From f0b136b5dc7243c3830b7fdeaecb20e09988158b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20J=C3=B6nsson?= Date: Sun, 12 Nov 2023 12:07:58 +0100 Subject: [PATCH 7/9] Add contact person to event page once user is booked --- src/features/events/l10n/messageIds.ts | 5 ++- .../o/[orgId]/events/[eventId]/index.tsx | 41 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/features/events/l10n/messageIds.ts b/src/features/events/l10n/messageIds.ts index 9b67dff06f..6e7b1b9c1e 100644 --- a/src/features/events/l10n/messageIds.ts +++ b/src/features/events/l10n/messageIds.ts @@ -5,11 +5,14 @@ export default makeMessages('feat.events', { bookedMessage: m( 'You are booked! If you want to cancel, reach out to contact person' ), + contactPerson: m('Responsible for this event'), joinOrgButton: m('Join organization'), loadingButton: m('Loading...'), loadingTitle: m('Loading event...'), + missingTitle: m('Nameless event'), notInOrgMessage: m('You are not member of this org yet'), - signedUp: m('Signed up!'), + showBigMap: m('Show on map'), + signedUp: m('Signed up'), signupButton: m('Sign up'), undoSignupButton: m('Undo signup'), }, diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index de7e444ca6..efd7babf9c 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -19,6 +19,7 @@ import { scaffold } from 'utils/next'; import useEvent from 'features/events/hooks/useEvent'; import useEventSignup from 'features/events/hooks/useEventSignup'; import useServerSide from 'core/useServerSide'; +import { ZetkinEvent } from 'utils/types/zetkin'; import ZUIDateTime from 'zui/ZUIDateTime'; import ZUIFuture from 'zui/ZUIFuture'; import { BeachAccess, CalendarToday, Done, Place } from '@mui/icons-material'; @@ -50,6 +51,27 @@ const Map = dynamic( { ssr: false } ); +const ContactPerson: FC> = ({ contact = null }) => + contact && ( + + + + + + + {contact.name} + + + ); + const Page: FC = ({ orgId, eventId }) => { const eventFuture = useEvent(parseInt(orgId, 10), parseInt(eventId, 10)); const onServer = useServerSide(); @@ -76,10 +98,18 @@ const Page: FC = ({ orgId, eventId }) => { return ( <> - {event.title} + + {event.title || ( + <Msg id={messageIds.activistPortal.missingTitle} /> + )} + - - {event.title} + + + {event.title || ( + + )} + @@ -108,7 +138,7 @@ const Page: FC = ({ orgId, eventId }) => { onClick={() => setShowBigMap(true)} underline="hover" > - Show on map + = ({ orgId, eventId }) => { )} - {event.info_text} + {event.info_text && {event.info_text}} {location && ( = ({ orgId, eventId }) => { ) : myResponseState == 'booked' ? ( <> + From 27b981ff89d572be8a3a78a10a0e70d7c67b8d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20J=C3=B6nsson?= Date: Sun, 12 Nov 2023 12:22:41 +0100 Subject: [PATCH 8/9] Move modal code in event page --- .../o/[orgId]/events/[eventId]/index.tsx | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index efd7babf9c..30854869dc 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -140,31 +140,6 @@ const Page: FC = ({ orgId, eventId }) => { > - setShowBigMap(false)} - open={showBigMap} - > - - - - )} @@ -232,6 +207,25 @@ const Page: FC = ({ orgId, eventId }) => { + setShowBigMap(false)} open={showBigMap}> + + + + ); }} From 2c1d77d65d702dac6f0cefb84d4e6c78618718fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20J=C3=B6nsson?= Date: Sun, 12 Nov 2023 12:34:33 +0100 Subject: [PATCH 9/9] Add link to external web page on event page --- src/features/events/l10n/messageIds.ts | 1 + .../o/[orgId]/events/[eventId]/index.tsx | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/features/events/l10n/messageIds.ts b/src/features/events/l10n/messageIds.ts index 6e7b1b9c1e..6b935dc3a9 100644 --- a/src/features/events/l10n/messageIds.ts +++ b/src/features/events/l10n/messageIds.ts @@ -7,6 +7,7 @@ export default makeMessages('feat.events', { ), contactPerson: m('Responsible for this event'), joinOrgButton: m('Join organization'), + linkText: m('Link to event information'), loadingButton: m('Loading...'), loadingTitle: m('Loading event...'), missingTitle: m('Nameless event'), diff --git a/src/pages/o/[orgId]/events/[eventId]/index.tsx b/src/pages/o/[orgId]/events/[eventId]/index.tsx index 30854869dc..ddff97d5b8 100644 --- a/src/pages/o/[orgId]/events/[eventId]/index.tsx +++ b/src/pages/o/[orgId]/events/[eventId]/index.tsx @@ -11,6 +11,13 @@ import { Modal, Typography, } from '@mui/material'; +import { + BeachAccess, + CalendarToday, + Done, + Link as LinkIcon, + Place, +} from '@mui/icons-material'; import { FC, useState } from 'react'; import messageIds from 'features/events/l10n/messageIds'; @@ -22,7 +29,6 @@ import useServerSide from 'core/useServerSide'; import { ZetkinEvent } from 'utils/types/zetkin'; import ZUIDateTime from 'zui/ZUIDateTime'; import ZUIFuture from 'zui/ZUIFuture'; -import { BeachAccess, CalendarToday, Done, Place } from '@mui/icons-material'; const scaffoldOptions = { allowNonMembers: true, @@ -142,6 +148,19 @@ const Page: FC = ({ orgId, eventId }) => { )} + {event.url && ( + + + + + + + )} {event.info_text && {event.info_text}} {location && (