Skip to content

Commit

Permalink
add activity dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelneu committed May 1, 2019
1 parent a42c414 commit 2bd109a
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 0 deletions.
142 changes: 142 additions & 0 deletions frontend/src/components/activity-event.tsx
@@ -0,0 +1,142 @@
import * as React from "react";
import { useState } from "react";
import styled from "styled-components";
import { ActivityType, IActivity } from "../../../types/activity";
import { borderRadius, transitionDuration } from "../config";
import { Nullable } from "../state";
import { IThemeProps } from "../theme";
import { dateToString } from "../util";
import { DiffEditor } from "./diff-editor";

const Container = styled.div`
margin-bottom: 1rem;
`;

interface IBodyProps {
shown: boolean;
}

const Body = styled.div<IBodyProps>`
opacity: 0;
padding: 0rem 1rem;
height: 0rem;
overflow: hidden;
${({ shown }) => shown && `
opacity: 1;
height: calc(40vh + 2rem);
padding: 1rem;
`}
border-radius: ${borderRadius};
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.05);
transition-property: height, opacity, padding;
transition-duration: ${transitionDuration};
`;

interface ITitleProps {
clickable: boolean;
}

const Title = styled.h3<ITitleProps>`
margin: 0rem;
font-weight: normal;
font-size: 0.9rem;
${({ clickable }) => clickable && `
cursor: pointer;
`}
`;

const Accent = styled.span`
color: ${({ theme }: IThemeProps) => theme.colorGradientEnd};
`;

const Time = styled.div`
margin-bottom: 1rem;
font-size: 0.75;
opacity: 0.5;
`;

interface IClickIndicatorProps {
clicked: boolean;
}

const ClickIndicator = styled.button<IClickIndicatorProps>`
display: inline-block;
margin: 0rem 0.5rem;
border: none;
color: currentColor;
background-color: transparent;
cursor: pointer;
transition-property: transform;
transition-duration: ${transitionDuration};
${({ clicked }) => clicked && `
transform: rotateZ(90deg);
`}
`;

const getActivityText = (type: ActivityType) => {
switch (type) {
case ActivityType.Signup:
return "Signup";

case ActivityType.EmailVerified:
return "E-mail verified";

case ActivityType.SettingsUpdate:
return "Settings updated";
}
};

interface IActivityEventProps {
event: IActivity;
}

/**
* An activity event.
*/
export const ActivityEvent = ({ event }: IActivityEventProps) => {
const [showBody, setShowBody] = useState(false);
let body: Nullable<JSX.Element> = null;

switch (event.data.type) {
case ActivityType.SettingsUpdate:
body = (
<DiffEditor
language="json"
left={event.data.previous}
right={event.data.next}
/>
);
break;
}

return (
<Container>
<Title
onClick={() => setShowBody((value) => !value)}
clickable={!!body}
>
{getActivityText(event.data.type)} by <Accent>{event.user.email}</Accent>

{body && (
<ClickIndicator clicked={showBody}>&#9658;</ClickIndicator>
)}
</Title>

<Time>
{dateToString(new Date(event.timestamp))}
</Time>

{body && (
<Body shown={showBody}>
{body}
</Body>
)}
</Container>
);
};
70 changes: 70 additions & 0 deletions frontend/src/components/activity.tsx
@@ -0,0 +1,70 @@
import * as React from "react";
import { useEffect } from "react";
import { connect } from "react-redux";
import { bindActionCreators, Dispatch } from "redux";
import { IActivity } from "../../../types/activity";
import { fetchActivities } from "../actions/activity";
import { IState } from "../state";
import { groupByMonth } from "../util";
import { ActivityEvent } from "./activity-event";
import { Heading, Subheading } from "./headings";
import { Placeholder } from "./placeholder";

interface IActivityProps {
activity: IActivity[] | null;
dispatchFetchActivities: typeof fetchActivities;
}

/**
* An overview over activities in tilt.
*/
export const Activity = ({ activity, dispatchFetchActivities }: IActivityProps) => {
useEffect(() => {
if (!activity) {
dispatchFetchActivities();
}
}, []);

const descendingActivities = (activity || []).sort((a, b) => b.timestamp - a.timestamp);
const activityByMonth =
groupByMonth(descendingActivities)
.map(({ month, data }) => (
<>
<Subheading>{month}</Subheading>
{data.map((event) => (
<ActivityEvent event={event} />
))}
</>
));

return (
<>
<Heading>Activity</Heading>

{!activity && (
<>
<Placeholder width="200px" height="0.7rem" />
<br />
<Placeholder width="100%" height="2rem" />
</>
)}

{activityByMonth}
</>
);
};

const mapStateToProps = (state: IState) => ({
activity: state.activity,
});

const mapDispatchToProps = (dispatch: Dispatch) => {
return bindActionCreators({
dispatchFetchActivities: fetchActivities,
}, dispatch);
};

/**
* The activity component connected to the redux store.
*/
export const ConnectedActivity = connect(mapStateToProps, mapDispatchToProps)(Activity);
2 changes: 2 additions & 0 deletions frontend/src/components/dashboard.tsx
Expand Up @@ -4,6 +4,7 @@ import { Route, Switch } from "react-router";
import styled from "styled-components";
import { sidebarWidth, transitionDuration } from "../config";
import { Routes } from "../routes";
import { ConnectedActivity } from "./activity";
import { PageSizedContainer } from "./centering";
import { ConnectedNotification } from "./notification";
import { PageNotFound } from "./page-not-found";
Expand Down Expand Up @@ -95,6 +96,7 @@ export const Dashboard = () => {
<ContentContainer>
<ConnectedNotification />
<Switch>
<Route path={Routes.Activity} component={ConnectedActivity} />
<Route path={Routes.Settings} component={Settings} />
<Route component={PageNotFound} />
</Switch>
Expand Down
48 changes: 48 additions & 0 deletions frontend/src/util.ts
Expand Up @@ -3,3 +3,51 @@
* @param ms The duration to sleep
*/
export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

/**
* Gets the month name of the given date, e.g. January for "1970-01-01".
* @param date A date to get the month from
*/
export const getMonthName = (date: Date) => date.toLocaleDateString("en", { month: "long" });

interface ITimestamped {
timestamp: number;
}

interface IMonthlyGroupedData<T> {
month: string;
data: T[];
}

/**
* Groups data based on its timestamp into months.
* @param data Data to group
*/
export const groupByMonth = <T extends ITimestamped>(data: T[]): Array<IMonthlyGroupedData<T>> => (
data.reduce<Array<IMonthlyGroupedData<T>>>((months, value) => {
const date = new Date(value.timestamp);
const monthName = getMonthName(date);
const name = `${monthName} ${date.getFullYear()}`;
const previousMonth = months[months.length - 1];

if (!previousMonth || previousMonth.month !== name) {
months.push({
data: [],
month: name,
});
}

months[months.length - 1].data.push(value);
return months;
}, [])
);

const prependZero = (value: number): string => value < 10 ? `0${value}` : `${value}`;

/**
* Formats a date "YYYY-MM-DD on HH:mm:ss" style.
* @param date The date to format
*/
export const dateToString = (date: Date) => (
`${date.getFullYear()}-${prependZero(date.getMonth() + 1)}-${prependZero(date.getDate())} at ${prependZero(date.getHours())}:${prependZero(date.getMinutes())}:${prependZero(date.getSeconds())}`
);

0 comments on commit 2bd109a

Please sign in to comment.