diff --git a/cypress/fixtures/dummyEventResponses.json b/cypress/fixtures/dummyEventResponses.json new file mode 100644 index 000000000..ff73c36c2 --- /dev/null +++ b/cypress/fixtures/dummyEventResponses.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "action_id": 25, + "response_date": "2021-01-21T18:30:00+00:00", + "organization_id": 1, + "person": { + "name": "Dummy User", + "id": 2 + }, + "id": 2 + } + ] +} \ No newline at end of file diff --git a/cypress/integration/org_event_page.spec.ts b/cypress/integration/org_event_page.spec.ts index ad80a36ad..dec1034ef 100644 --- a/cypress/integration/org_event_page.spec.ts +++ b/cypress/integration/org_event_page.spec.ts @@ -1,28 +1,79 @@ describe('/o/[orgId]/events/[eventId]', () => { + beforeEach(() => { + cy.request('delete', 'http://localhost:8001/_mocks'); + }); + + after(() => { + cy.request('delete', 'http://localhost:8001/_mocks'); + }); + it('contains non-interactive event content', () => { - cy.visit('/o/1/events/22'); + cy.visit('/o/1/events/25'); cy.get('[data-test="event-title"]').should('be.visible'); cy.get('[data-test="duration"]').should('be.visible'); cy.get('[data-test="location"]').should('be.visible'); }); it('contains clickable org name that leads to org page', () => { - cy.visit('/o/1/events/22'); + cy.visit('/o/1/events/25'); cy.waitUntilReactRendered(); cy.findByText('My Organization').click(); cy.url().should('match', /\/o\/1$/); }); it('contains clickable campaign name that leads to campaign page', () => { - cy.visit('/o/1/events/22'); + cy.visit('/o/1/events/25'); cy.waitUntilReactRendered(); cy.findByText('Second campaign').click(); cy.url().should('match', /\/o\/1\/campaigns\/2$/); }); - it('contains a sign-up button', () => { - cy.visit('/o/1/events/22'); - cy.findByText('Sign-up').should('be.visible'); + it('shows a sign-up button if user is not signed up to the event', () => { + cy.request('put', 'http://localhost:8001/v1/users/me/action_responses/_mocks/get', { + response: { + data: { + data: [], + }, + }, + }); + + cy.visit('/login'); + cy.get('input[aria-label="E-mail address"]').type('testadmin@example.com'); + cy.get('input[aria-label="Password"]').type('password'); + cy.get('input[aria-label="Log in"]') + .click(); + + cy.visit('/o/1/events/25'); + cy.waitUntilReactRendered(); + cy.findByText('Sign-up').click(); + //TODO: Verify that API request is done corrently. + }); + + it('shows an undo sign-up button if user is signed up to the event', () => { + cy.fixture('dummyEventResponses').then(json => { + cy.request('put', 'http://localhost:8001/v1/users/me/action_responses/_mocks/get', { + response: { + data: json, + }, + }); + + cy.request('put', 'http://localhost:8001/v1/orgs/1/actions/25/responses/2/_mocks/delete', { + response: { + status: 204, + }, + }); + + cy.visit('/login'); + cy.get('input[aria-label="E-mail address"]').type('testadmin@example.com'); + cy.get('input[aria-label="Password"]').type('password'); + cy.get('input[aria-label="Log in"]') + .click(); + + cy.visit('/o/1/events/25'); + cy.waitUntilReactRendered(); + cy.findByText('Undo sign-up').click(); + //TODO: Verify that API request is done corrently. + }); }); }); diff --git a/cypress/integration/org_events_page.spec.ts b/cypress/integration/org_events_page.spec.ts index 4635b7327..6cf25f47a 100644 --- a/cypress/integration/org_events_page.spec.ts +++ b/cypress/integration/org_events_page.spec.ts @@ -3,12 +3,17 @@ describe('/o/[orgId]/events', () => { cy.request('delete', 'http://localhost:8001/_mocks'); }); + after(() => { + cy.request('delete', 'http://localhost:8001/_mocks'); + }); + it('contains name of organization', () => { cy.visit('/o/1/events'); + cy.waitUntilReactRendered(); cy.contains('My Organization'); }); - it.only('contains events which are linked to event pages', () => { + it('contains events which are linked to event pages', () => { cy.request('put', 'http://localhost:8001/v1/orgs/1/campaigns/_mocks/get', { response: { data: { @@ -38,7 +43,7 @@ describe('/o/[orgId]/events', () => { }); it('contains a placeholder if there are no events', () => { - cy.request('put', 'http://localhost:8001/v1/orgs/1/campaigns/1/actions/_mocks/get', { + cy.request('put', 'http://localhost:8001/v1/orgs/1/campaigns/_mocks/get', { response: { data: { data: [], @@ -49,6 +54,56 @@ describe('/o/[orgId]/events', () => { cy.visit('/o/1/events'); cy.get('[data-test="no-events-placeholder"]').should('be.visible'); }); + + it('shows a sign-up button if user is not signed up to an event', () => { + cy.request('put', 'http://localhost:8001/v1/users/me/action_responses/_mocks/get', { + response: { + data: { + data: [], + }, + }, + }); + + cy.visit('/login'); + cy.get('input[aria-label="E-mail address"]').type('testadmin@example.com'); + cy.get('input[aria-label="Password"]').type('password'); + cy.get('input[aria-label="Log in"]') + .click(); + + cy.visit('/o/1/events'); + cy.waitUntilReactRendered(); + cy.get('[data-test="event-response-button"]') + .eq(4) + .click(); + //TODO: Verify that API request is done corrently. + }); + + it('shows an undo sign-up button if user is signed up to an event', () => { + cy.fixture('dummyEventResponses').then(json => { + cy.request('put', 'http://localhost:8001/v1/users/me/action_responses/_mocks/get', { + response: { + data: json, + }, + }); + + cy.request('put', 'http://localhost:8001/v1/orgs/1/actions/25/responses/2/_mocks/delete', { + response: { + status: 204, + }, + }); + + cy.visit('/login'); + cy.get('input[aria-label="E-mail address"]').type('testadmin@example.com'); + cy.get('input[aria-label="Password"]').type('password'); + cy.get('input[aria-label="Log in"]') + .click(); + + cy.visit('/o/1/events'); + cy.waitUntilReactRendered(); + cy.findByText('Undo sign-up').click(); + //TODO: Verify that API request is done corrently. + }); + }); }); // Hack to flag for typescript as module diff --git a/cypress/integration/reocurring_functionality.spec.ts b/cypress/integration/reocurring_functionality.spec.ts index 22ed57747..2375c31eb 100644 --- a/cypress/integration/reocurring_functionality.spec.ts +++ b/cypress/integration/reocurring_functionality.spec.ts @@ -1,4 +1,5 @@ describe('Reocurring functionality', () => { + it('contains a clickable org logo which leads to org page', () => { cy.visit('/o/1/events'); cy.get('[data-test="org-avatar"]') diff --git a/src/components/EventList.spec.tsx b/src/components/EventList.spec.tsx index 1cc664fe2..8c6994685 100644 --- a/src/components/EventList.spec.tsx +++ b/src/components/EventList.spec.tsx @@ -1,11 +1,13 @@ import EventList from './EventList'; import { mountWithProviders } from '../utils/testing'; import { ZetkinEvent } from '../interfaces/ZetkinEvent'; +import { ZetkinEventResponse } from '../types/zetkin'; import { ZetkinOrganization } from '../interfaces/ZetkinOrganization'; describe('EventList', () => { let dummyOrg : ZetkinOrganization; let dummyEvents : ZetkinEvent[]; + let dummyEventResponses : ZetkinEventResponse[]; beforeEach(()=> { cy.fixture('dummyOrg.json') @@ -16,11 +18,20 @@ describe('EventList', () => { .then((data : {data: ZetkinEvent[]}) => { dummyEvents = data.data; }); + cy.fixture('dummyEventResponses.json') + .then((data : {data: ZetkinEventResponse[]}) => { + dummyEventResponses = data.data; + }); }); it('contains data for each event', () => { mountWithProviders( - , + null } + org={ dummyOrg } + />, ); cy.get('[data-test="event"]').each((item) => { @@ -36,8 +47,14 @@ describe('EventList', () => { it('contains an activity title instead of missing event title', () => { dummyEvents[0].title = undefined; + mountWithProviders( - , + null } + org={ dummyOrg } + />, ); cy.get('[data-test="event"]') @@ -46,16 +63,33 @@ describe('EventList', () => { }); it('contains a sign-up button for each event', () => { + const spyOnSubmit = cy.spy(); + mountWithProviders( - , + , ); - cy.contains('misc.eventList.signup'); + cy.findByText('misc.eventList.signup') + .eq(0) + .click() + .then(() => { + expect(spyOnSubmit).to.be.calledOnce; + }); }); it('contains a button for more info on each event', () => { mountWithProviders( - , + null } + org={ dummyOrg } + />, ); cy.contains('misc.eventList.moreInfo'); @@ -63,8 +97,14 @@ describe('EventList', () => { it('shows a placeholder when the list is empty', () => { dummyEvents = []; + mountWithProviders( - , + null } + org={ dummyOrg } + />, ); cy.contains('misc.eventList.placeholder'); @@ -72,7 +112,12 @@ describe('EventList', () => { it('shows a placeholder when the list is undefined', () => { mountWithProviders( - , + null } + org={ dummyOrg } + />, ); cy.contains('misc.eventList.placeholder'); diff --git a/src/components/EventList.tsx b/src/components/EventList.tsx index cce5f0b33..3267b6d6e 100644 --- a/src/components/EventList.tsx +++ b/src/components/EventList.tsx @@ -12,14 +12,18 @@ import { } from 'react-intl'; import { ZetkinEvent } from '../interfaces/ZetkinEvent'; +import { ZetkinEventResponse } from '../types/zetkin'; import { ZetkinOrganization } from '../interfaces/ZetkinOrganization'; interface EventListProps { events: ZetkinEvent[] | undefined; - org: ZetkinOrganization | undefined; + org: ZetkinOrganization; + eventResponses: ZetkinEventResponse[] | undefined; + onEventResponse: (eventId: number, orgId: number, response: boolean) => void; } -const EventList = ({ events, org } : EventListProps) : JSX.Element => { +const EventList = ({ eventResponses, events, onEventResponse, org } : EventListProps) : JSX.Element => { + if (!events || events.length === 0) { return ( @@ -31,46 +35,63 @@ const EventList = ({ events, org } : EventListProps) : JSX.Element => { return ( <> - { events?.map((e) => ( - - - { e.title ? e.title : e.activity.title } - - { org?.title } - { e.campaign.title } - - - , - - - - , - - { e.location.title } - - - - + ) : ( + - - - - )) } + ) } + + + + + + + ); + }) } ); diff --git a/src/fetching/deleteEventResponse.ts b/src/fetching/deleteEventResponse.ts new file mode 100644 index 000000000..52001ba09 --- /dev/null +++ b/src/fetching/deleteEventResponse.ts @@ -0,0 +1,20 @@ +import apiUrl from '../utils/apiUrl'; +import { ZetkinMembership } from '../types/zetkin'; + +interface MutationVariables { + eventId: number; + orgId: number; +} + +export default async function deleteEventResponse({ eventId, orgId } : MutationVariables) : Promise { + const mRes = await fetch(apiUrl('/users/me/memberships')); + const mData = await mRes.json(); + //TODO: Memberships should be cached. + const orgMembership = mData.data.find((m : ZetkinMembership ) => m.organization.id === orgId); + + if (orgMembership) { + await fetch(apiUrl(`/orgs/${orgId}/actions/${eventId}/responses/${orgMembership.profile.id}`), { + method: 'DELETE', + }); + } +} \ No newline at end of file diff --git a/src/fetching/getEvent.ts b/src/fetching/getEvent.ts index 56968a629..0c864da0c 100644 --- a/src/fetching/getEvent.ts +++ b/src/fetching/getEvent.ts @@ -1,13 +1,13 @@ -import apiUrl from '../utils/apiUrl'; +import { defaultFetch } from '.'; import { ZetkinEvent } from '../interfaces/ZetkinEvent'; -export default function getEvent(orgId : string, eventId : string) { +export default function getEvent(orgId : string, eventId : string, fetch = defaultFetch) { return async () : Promise => { - const cRes = await fetch(apiUrl(`/orgs/${orgId}/campaigns`)); + const cRes = await fetch(`/orgs/${orgId}/campaigns`); const cData = await cRes.json(); for (const obj of cData.data) { - const eventsRes = await fetch(apiUrl(`/orgs/${orgId}/campaigns/${obj.id}/actions`)); + const eventsRes = await fetch(`/orgs/${orgId}/campaigns/${obj.id}/actions`); const campaignEvents = await eventsRes.json(); const eventData = campaignEvents.data.find((event : ZetkinEvent) => event.id.toString() === eventId); if (eventData) { diff --git a/src/fetching/getEventResponses.ts b/src/fetching/getEventResponses.ts new file mode 100644 index 000000000..f3d55b312 --- /dev/null +++ b/src/fetching/getEventResponses.ts @@ -0,0 +1,10 @@ +import { defaultFetch } from '.'; +import { ZetkinEventResponse } from '../types/zetkin'; + +export default function getEventResponses(fetch = defaultFetch) { + return async () : Promise => { + const rRes = await fetch('/users/me/action_responses'); + const rData = await rRes.json(); + return rData.data; + }; +} \ No newline at end of file diff --git a/src/fetching/getEvents.ts b/src/fetching/getEvents.ts index aae159590..c49d99e00 100644 --- a/src/fetching/getEvents.ts +++ b/src/fetching/getEvents.ts @@ -1,15 +1,15 @@ -import apiUrl from '../utils/apiUrl'; +import { defaultFetch } from '.'; import { ZetkinEvent } from '../interfaces/ZetkinEvent'; -export default function getEvents(orgId : string) { +export default function getEvents(orgId : string, fetch = defaultFetch) { return async () : Promise => { - const cRes = await fetch(apiUrl(`/orgs/${orgId}/campaigns`)); + const cRes = await fetch(`/orgs/${orgId}/campaigns`); const cData = await cRes.json(); let allEventsData : ZetkinEvent[] = []; for (const obj of cData.data) { - const eventsRes = await fetch(apiUrl(`/orgs/${orgId}/campaigns/${obj.id}/actions`)); + const eventsRes = await fetch(`/orgs/${orgId}/campaigns/${obj.id}/actions`); const campaignEvents = await eventsRes.json(); allEventsData = allEventsData.concat(campaignEvents.data); } diff --git a/src/fetching/getOrg.ts b/src/fetching/getOrg.ts index cc593bcb5..4d8028b9d 100644 --- a/src/fetching/getOrg.ts +++ b/src/fetching/getOrg.ts @@ -1,11 +1,9 @@ -import apiUrl from '../utils/apiUrl'; - +import { defaultFetch } from '.'; import { ZetkinOrganization } from '../interfaces/ZetkinOrganization'; -export default function getOrg(orgId : string) { +export default function getOrg(orgId : string, fetch = defaultFetch) { return async () : Promise => { - const url = apiUrl(`/orgs/${orgId}`); - const oRes = await fetch(url); + const oRes = await fetch(`/orgs/${orgId}`); const oData = await oRes.json(); return oData.data; }; diff --git a/src/fetching/index.ts b/src/fetching/index.ts new file mode 100644 index 000000000..0a1f0c4dc --- /dev/null +++ b/src/fetching/index.ts @@ -0,0 +1,6 @@ +import apiUrl from '../utils/apiUrl'; + +export function defaultFetch(path : string, init? : RequestInit) : Promise { + const url = apiUrl(path); + return fetch(url, init); +} \ No newline at end of file diff --git a/src/fetching/putEventResponse.ts b/src/fetching/putEventResponse.ts new file mode 100644 index 000000000..21c15716a --- /dev/null +++ b/src/fetching/putEventResponse.ts @@ -0,0 +1,25 @@ +import apiUrl from '../utils/apiUrl'; +import { ZetkinEventResponse, ZetkinMembership } from '../types/zetkin'; + +interface MutationVariables { + eventId: number; + orgId: number; +} + +export default async function putEventResponse({ eventId, orgId } : MutationVariables) : Promise { + const mRes = await fetch(apiUrl('/users/me/memberships')); + const mData = await mRes.json(); + //TODO: Memberships should be cached. + const orgMembership = mData.data.find((m : ZetkinMembership ) => m.organization.id === orgId); + + if (orgMembership) { + const eventRes = await fetch(apiUrl(`/orgs/${orgId}/actions/${eventId}/responses/${orgMembership.profile.id}`), { + method: 'PUT', + }); + const eventResData = await eventRes.json(); + + return eventResData.data; + } + + throw 'no membership'; +} \ No newline at end of file diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 4bff73f92..92d51980c 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,8 +1,51 @@ import React from 'react'; +import { useMutation, useQuery, useQueryClient } from 'react-query'; + +import deleteEventResponse from '../fetching/deleteEventResponse'; +import getEventResponses from '../fetching/getEventResponses'; +import putEventResponse from '../fetching/putEventResponse'; +import { ZetkinEventResponse } from '../types/zetkin'; import { ZetkinUser } from '../interfaces/ZetkinUser'; export const UserContext = React.createContext(null); export const useUser = () : ZetkinUser | null => { return React.useContext(UserContext); +}; + +type OnEventResponse = (eventId : number, orgId : number, response : boolean) => void; + +type EventResponses = { + eventResponses: ZetkinEventResponse[] | undefined; + onEventResponse: OnEventResponse; +} + +export const useEventResponses = () : EventResponses => { + const responseQuery = useQuery('eventResponses', getEventResponses()); + const eventResponses = responseQuery.data; + + const queryClient = useQueryClient(); + + const removeFunc = useMutation(deleteEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + const addFunc = useMutation(putEventResponse, { + onSettled: () => { + queryClient.invalidateQueries('eventResponses'); + }, + }); + + function onEventResponse (eventId : number, orgId : number, response : boolean) { + if (response) { + removeFunc.mutate({ eventId, orgId }); + } + else { + addFunc.mutate({ eventId, orgId }); + } + } + + return { eventResponses, onEventResponse }; }; \ No newline at end of file diff --git a/src/locale/misc/eventList/en.yml b/src/locale/misc/eventList/en.yml index 01db07ec1..e60a72153 100644 --- a/src/locale/misc/eventList/en.yml +++ b/src/locale/misc/eventList/en.yml @@ -1,3 +1,4 @@ placeholder: Sorry, there are no planned events at the moment. moreInfo: More info -signup: Sign-up \ No newline at end of file +signup: Sign-up +undoSignup: Undo sign-up \ No newline at end of file diff --git a/src/locale/misc/eventList/sv.yml b/src/locale/misc/eventList/sv.yml index f30b97b2c..be3ee8f90 100644 --- a/src/locale/misc/eventList/sv.yml +++ b/src/locale/misc/eventList/sv.yml @@ -1,3 +1,4 @@ placeholder: Tyvärr finns det inga planerade händelser för tillfället. moreInfo: Mer info -signup: Anmälan \ No newline at end of file +signup: Anmälan +undoSignup: Ångra anmälan \ No newline at end of file diff --git a/src/locale/pages/orgEvent/en.yml b/src/locale/pages/orgEvent/en.yml index fe4b19989..d6014be83 100644 --- a/src/locale/pages/orgEvent/en.yml +++ b/src/locale/pages/orgEvent/en.yml @@ -1,2 +1,3 @@ actions: - signUp: Sign-up \ No newline at end of file + signup: Sign-up + undoSignup: Undo sign-up diff --git a/src/locale/pages/orgEvent/sv.yml b/src/locale/pages/orgEvent/sv.yml index 94a204a7d..b3e65f08a 100644 --- a/src/locale/pages/orgEvent/sv.yml +++ b/src/locale/pages/orgEvent/sv.yml @@ -1,2 +1,3 @@ actions: - signUp: Anmälan + signup: Anmälan + undoSignup: Ångra anmälan \ No newline at end of file diff --git a/src/pages/o/[orgId]/campaigns/[campId].tsx b/src/pages/o/[orgId]/campaigns/[campId].tsx index 033137772..c667b5cfd 100644 --- a/src/pages/o/[orgId]/campaigns/[campId].tsx +++ b/src/pages/o/[orgId]/campaigns/[campId].tsx @@ -10,6 +10,7 @@ import getCampaignEvents from '../../../../fetching/getCampaignEvents'; import getOrg from '../../../../fetching/getOrg'; import { PageWithLayout } from '../../../../types'; import { scaffold } from '../../../../utils/next'; +import { useEventResponses } from '../../../../hooks'; export const getServerSideProps : GetServerSideProps = scaffold(async (context) => { const queryClient = new QueryClient(); @@ -51,6 +52,8 @@ const OrgCampaignPage : PageWithLayout = (props) => { const orgQuery = useQuery(['org', orgId], getOrg(orgId)); const campaignEventsQuery = useQuery(['campaignEvents', campId], getCampaignEvents(orgId, campId)); + const { eventResponses, onEventResponse } = useEventResponses(); + return ( @@ -60,8 +63,11 @@ const OrgCampaignPage : PageWithLayout = (props) => { { campaignQuery.data?.info_text } ); diff --git a/src/pages/o/[orgId]/events.tsx b/src/pages/o/[orgId]/events.tsx index c8a3420d1..ad757faa3 100644 --- a/src/pages/o/[orgId]/events.tsx +++ b/src/pages/o/[orgId]/events.tsx @@ -4,11 +4,13 @@ import { GetServerSideProps } from 'next'; import { QueryClient, useQuery } from 'react-query'; import EventList from '../../../components/EventList'; +import getEventResponses from '../../../fetching/getEventResponses'; import getEvents from '../../../fetching/getEvents'; import getOrg from '../../../fetching/getOrg'; import MainOrgLayout from '../../../components/layout/MainOrgLayout'; import { PageWithLayout } from '../../../types'; import { scaffold } from '../../../utils/next'; +import { useEventResponses } from '../../../hooks'; const scaffoldOptions = { localeScope: [ @@ -22,9 +24,14 @@ export const getServerSideProps : GetServerSideProps = scaffold(async (context) const queryClient = new QueryClient(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { orgId } = context.params!; + const { user } = context; - await queryClient.prefetchQuery('events', getEvents(orgId as string)); - await queryClient.prefetchQuery(['org', orgId], getOrg(orgId as string)); + await queryClient.prefetchQuery('events', getEvents(orgId as string, context.apiFetch)); + await queryClient.prefetchQuery(['org', orgId], getOrg(orgId as string, context.apiFetch)); + + if (user) { + await queryClient.prefetchQuery('eventResponses', getEventResponses(context.apiFetch)); + } const eventsState = queryClient.getQueryState('events'); const orgState = queryClient.getQueryState(['org', orgId]); @@ -53,11 +60,16 @@ const OrgEventsPage : PageWithLayout = (props) => { const eventsQuery = useQuery('events', getEvents(orgId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); + const { eventResponses, onEventResponse } = useEventResponses(); + return ( ); diff --git a/src/pages/o/[orgId]/events/[eventId].tsx b/src/pages/o/[orgId]/events/[eventId].tsx index 16f694aaa..d3a88cbf6 100644 --- a/src/pages/o/[orgId]/events/[eventId].tsx +++ b/src/pages/o/[orgId]/events/[eventId].tsx @@ -24,9 +24,11 @@ import { QueryClient, useQuery } from 'react-query'; import DefaultOrgLayout from '../../../../components/layout/DefaultOrgLayout'; import getEvent from '../../../../fetching/getEvent'; +import getEventResponses from '../../../../fetching/getEventResponses'; import getOrg from '../../../../fetching/getOrg'; import { PageWithLayout } from '../../../../types'; import { scaffold } from '../../../../utils/next'; +import { useEventResponses } from '../../../../hooks'; import { ZetkinEvent } from '../../../../interfaces/ZetkinEvent'; import { ZetkinOrganization } from '../../../../interfaces/ZetkinOrganization'; @@ -41,9 +43,17 @@ export const getServerSideProps : GetServerSideProps = scaffold(async (context) const queryClient = new QueryClient(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const { orgId, eventId } = context.params!; + const { user } = context; - await queryClient.prefetchQuery(['event', eventId], getEvent(orgId as string, eventId as string)); - await queryClient.prefetchQuery(['org', orgId], getOrg(orgId as string)); + await queryClient.prefetchQuery(['event', eventId], getEvent(orgId as string, eventId as string, context.apiFetch)); + await queryClient.prefetchQuery(['org', orgId], getOrg(orgId as string, context.apiFetch)); + + if (user) { + await queryClient.prefetchQuery('eventResponses', getEventResponses(context.apiFetch)); + } + else { + null; + } const eventState = queryClient.getQueryState(['event', eventId]); const orgState = queryClient.getQueryState(['org', orgId]); @@ -73,6 +83,7 @@ const OrgEventPage : PageWithLayout = (props) => { const { orgId, eventId } = props; const eventQuery = useQuery(['event', eventId], getEvent(orgId, eventId)); const orgQuery = useQuery(['org', orgId], getOrg(orgId)); + const { eventResponses, onEventResponse } = useEventResponses(); if (!eventQuery.data) { return null; @@ -81,6 +92,8 @@ const OrgEventPage : PageWithLayout = (props) => { const event = eventQuery.data as ZetkinEvent; const org = orgQuery.data as ZetkinOrganization; + const response = eventResponses?.find(response => response.action_id === event.id); + return ( <>
@@ -151,9 +164,21 @@ const OrgEventPage : PageWithLayout = (props) => { marginTop="size-200" position="absolute" right="size-200"> - + { response ? ( + + ) : ( + + ) } ); diff --git a/src/types/zetkin.ts b/src/types/zetkin.ts new file mode 100644 index 000000000..1398fa1aa --- /dev/null +++ b/src/types/zetkin.ts @@ -0,0 +1,19 @@ +export interface ZetkinMembership { + organization: { + id: number; + title: string; + }; + profile: { + id: number; + }; +} + +export interface ZetkinEventResponse { + action_id: number; + response_date: string; + person: { + name: string; + id: number; + }; + id: number; +} \ No newline at end of file diff --git a/src/utils/next.ts b/src/utils/next.ts index a1db9017d..3fc43cf97 100644 --- a/src/utils/next.ts +++ b/src/utils/next.ts @@ -26,6 +26,7 @@ export type ScaffoldedProps = RegularProps & { export type ScaffoldedContext = GetServerSidePropsContext & { apiFetch: (path : string, init? : RequestInit) => Promise; + user: ZetkinUser | null; z: ZetkinZ; }; @@ -74,6 +75,16 @@ export const scaffold = (wrapped : ScaffoldedGetServerSideProps, options? : Scaf ctx.z.setTokenData(reqWithSession.session.tokenData); } + try { + const userRes = await ctx.z.resource('users', 'me').get(); + ctx.user = userRes.data.data as ZetkinUser; + } + catch (error) { + ctx.user = null; + } + + const user = ctx.user; + const result = await wrapped(ctx); // Figure out browser's preferred language @@ -83,24 +94,14 @@ export const scaffold = (wrapped : ScaffoldedGetServerSideProps, options? : Scaf const messages = await getMessages(lang, options?.localeScope ?? []); - const augmentProps = (user : ZetkinUser | null) => { - if (hasProps(result)) { - const scaffoldedProps : ScaffoldedProps = { - ...result.props, - lang, - messages, - user, - }; - result.props = scaffoldedProps; - } - }; - - try { - const user = await ctx.z.resource('users', 'me').get(); - augmentProps(user.data.data as ZetkinUser); - } - catch (error) { - augmentProps(null); + if (hasProps(result)) { + const scaffoldedProps : ScaffoldedProps = { + ...result.props, + lang, + messages, + user, + }; + result.props = scaffoldedProps; } return result as GetServerSidePropsResult;