Skip to content

Commit

Permalink
Implement Plausible analytics into event statistics
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarnakken committed Apr 26, 2023
1 parent 553f6e1 commit 1ed4a9e
Show file tree
Hide file tree
Showing 8 changed files with 194 additions and 21 deletions.
11 changes: 11 additions & 0 deletions app/actions/EventActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
eventAdministrateSchema,
followersEventSchema,
} from 'app/reducers';
import type { ID } from 'app/store/models';
import type { Thunk, Action } from 'app/types';
import createQueryString from 'app/utils/createQueryString';
import { Event } from './ActionTypes';
Expand Down Expand Up @@ -418,3 +419,13 @@ export function isUserFollowing(eventId: number): Thunk<any> {
},
});
}
export function fetchAnalytics(eventId: ID): Thunk<Promise<Action>> {
return callAPI({
types: Event.FETCH,
endpoint: `/events/${String(eventId)}/statistics/`,
method: 'GET',
meta: {
errorMessage: 'Henting av analyse feilet',
},
});
}
2 changes: 1 addition & 1 deletion app/components/Chart/PieChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const DistributionPieChart = ({
distributionData,
}: {
distributionData: DistributionDataPoint[];
chartColors: string[];
chartColors?: string[];
dataKey: string;
}) => {
return (
Expand Down
2 changes: 1 addition & 1 deletion app/reducers/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import moment from 'moment-timezone';
import { normalize } from 'normalizr';
import { createSelector } from 'reselect';
import config from 'app/config';
import type { Event as EventType } from 'app/models';
import { eventSchema } from 'app/reducers';
import { mutateComments } from 'app/reducers/comments';
import { isCurrentUser as checkIfCurrentUser } from 'app/routes/users/utils';
import createEntityReducer from 'app/utils/createEntityReducer';
import joinReducers from 'app/utils/joinReducers';
import mergeObjects from 'app/utils/mergeObjects';
import { Event } from '../actions/ActionTypes';
import type { Event as EventType, PhotoConsent } from '../models';

type State = any;
const mutateEvent = produce((newState: State, action: any): void => {
Expand Down
1 change: 0 additions & 1 deletion app/routes/events/EventListRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { createStructuredSelector } from 'reselect';
import { fetchList } from 'app/actions/EventActions';
import { selectSortedEvents } from 'app/reducers/events';
import createQueryString from 'app/utils/createQueryString';
import loadingIndicator from 'app/utils/loadingIndicator';
import withPreparedDispatch from 'app/utils/withPreparedDispatch';
import { selectPagination } from '../../reducers/selectors';
import EventList from './components/EventList';
Expand Down
8 changes: 0 additions & 8 deletions app/routes/events/components/Event.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,3 @@
margin: 0 auto;
border-bottom: 1px solid #6772e585;
}

.noRegistrationsText {
text-align: center;
}

.chartContainer {
overflow: hidden;
}
6 changes: 4 additions & 2 deletions app/routes/events/components/EventAdministrate/Statistics.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Event, EventRegistration, Group } from 'app/models';
import type { EventRegistration, Group } from 'app/models';
import EventAttendeeStatistics from 'app/routes/events/components/EventAttendeeStatistics';
import type { DetailedEvent } from 'app/store/models/Event';

interface Props {
committees: Group[];
revueGroups: Group[];
registered: EventRegistration[];
unregistered: EventRegistration[];
event: Event;
event: DetailedEvent;
}

const Statistics = ({
Expand All @@ -18,6 +19,7 @@ const Statistics = ({
}: Props) => {
return (
<EventAttendeeStatistics
eventId={event.id}
registrations={registered}
unregistrations={unregistered}
committeeGroupIDs={committees.map((group) => group.id)}
Expand Down
31 changes: 31 additions & 0 deletions app/routes/events/components/EventAttendeeStatistics.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
.metrics {
height: 80px;
line-height: 1.3;
margin-bottom: 1rem;
}

.metricHeader {
font-weight: 500;
text-transform: uppercase;
color: var(--secondary-font-color);
}

.metricNumber {
font-size: 2rem;
font-weight: 500;
}

.metricDivider {
width: 2px;
height: 100%;
background-color: var(--border-gray);
}

.noRegistrationsText {
text-align: center;
color: var(--secondary-font-color);
}

.chartContainer {
overflow: hidden;
}
154 changes: 146 additions & 8 deletions app/routes/events/components/EventAttendeeStatistics.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import moment from 'moment';
import { useEffect, useState } from 'react';
import {
AreaChart,
Area,
CartesianGrid,
Legend,
Line,
LineChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { fetchAnalytics } from 'app/actions/EventActions';
import Card from 'app/components/Card';
import ChartLabel from 'app/components/Chart/ChartLabel';
import DistributionPieChart from 'app/components/Chart/PieChart';
import type { DistributionDataPoint } from 'app/components/Chart/utils';
import { Flex } from 'app/components/Layout';
import type { Dateish, EventRegistration } from 'app/models';
import styles from './Event.css';
import { useAppDispatch } from 'app/store/hooks';
import type { ID } from 'app/store/models';
import styles from './EventAttendeeStatistics.css';

interface RegistrationDateDataPoint {
name: string;
Expand Down Expand Up @@ -216,19 +223,147 @@ const isEventFromPreviousSemester = (eventStartTime: Dateish): boolean => {
);
};

const Analytics = ({ eventId }: { eventId: ID }) => {
const [metrics, setMetrics] = useState<{
visitors: { title: string; value: number };
pageviews: { title: string; value: number };
visitDuration: { title: string; value: number };
}>({
visitors: { title: 'Besøkende', value: 0 },
pageviews: { title: 'Sidevisninger', value: 0 },
visitDuration: { title: 'Besøkstid', value: 0 },
});
const [data, setData] = useState<{ date: string; value: number }[]>([]);

const dispatch = useAppDispatch();

useEffect(() => {
eventId &&
dispatch(fetchAnalytics(eventId)).then((res) => {
const initialValue = {
data: [],
visitors: { title: 'Besøkende', value: 0 },
pageviews: { title: 'Sidevisninger', value: 0 },
visitDuration: { title: 'Besøkstid', value: 0 },
};

const result = res.payload.reduce((acc, item) => {
// Update data array
acc.data.push({
date: item.date,
value: item.visitors,
});

// Update metrics
acc.visitors.value += item.visitors;
acc.pageviews.value += item.pageviews;
acc.visitDuration.value += item.visitDuration;

return acc;
}, initialValue);

setData(result.data);
setMetrics({
visitors: result.visitors,
pageviews: result.pageviews,
visitDuration: result.visitDuration,
});
});
}, [eventId, dispatch]);

return (
<>
<Flex wrap gap={40} className={styles.metrics}>
{Object.values(metrics).map((metric, i) => (
<>
<Flex column justifyContent="center" key={metric.title}>
<span className={styles.metricHeader}>{metric.title}</span>
<span className={styles.metricNumber}>
{metric.value.toLocaleString('no-NO')}
</span>
</Flex>

{i !== Object.values(metrics).length - 1 && (
<div className={styles.metricDivider} />
)}
</>
))}
</Flex>

<ResponsiveContainer width="100%" aspect={2.0 / 0.8}>
<AreaChart data={data}>
<defs>
<linearGradient id="colorView" x1="0" y1="0" x2="0" y2="1">
<stop
offset="30%"
stopColor="var(--color-blue-5)"
stopOpacity={0.4}
/>
<stop
offset="75%"
stopColor="var(--color-blue-4)"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="var(--color-white)"
stopOpacity={0.2}
/>
</linearGradient>
</defs>
<Tooltip
contentStyle={{
borderColor: 'var(--border-gray)',
borderRadius: 'var(--border-radius-md)',
backgroundColor: 'var(--lego-card-color)',
}}
/>
<CartesianGrid
strokeDasharray="4 4"
stroke="var(--color-blue-5)"
opacity={0.4}
/>
<XAxis
dataKey="date"
tick={{ fill: 'var(--secondary-font-color)' }}
stroke="var(--border-gray)"
/>
<YAxis
tick={{ fill: 'var(--secondary-font-color)' }}
stroke="var(--border-gray)"
/>
<Area
name="Besøkende"
dataKey="value"
type="monotone"
stroke="var(--color-blue-5)"
strokeWidth={3}
strokeOpacity={1}
fill="url(#colorView)"
/>
</AreaChart>
</ResponsiveContainer>
</>
);
};

type Props = {
eventId: ID;
registrations: EventRegistration[];
unregistrations: EventRegistration[];
committeeGroupIDs: number[];
revueGroupIDs: number[];
eventStartTime: Dateish;
};

const EventAttendeeStatistics = ({
eventId,
registrations,
unregistrations,
committeeGroupIDs,
revueGroupIDs,
eventStartTime,
}: {
registrations: Array<EventRegistration>;
unregistrations: Array<EventRegistration>;
committeeGroupIDs: number[];
revueGroupIDs: number[];
eventStartTime: Dateish;
}) => {
}: Props) => {
const {
genderDistribution,
groupDistribution,
Expand All @@ -255,6 +390,9 @@ const EventAttendeeStatistics = ({
</span>
</Card>
)}

<Analytics eventId={eventId} />

{registrations.length === 0 ? (
<p className={styles.noRegistrationsText}>Ingen er påmeldt enda.</p>
) : (
Expand Down

0 comments on commit 1ed4a9e

Please sign in to comment.