tutorbookapp/tutorbook

View on GitHub
components/analytics/index.tsx

Summary

Maintainability
F
6 days
Test Coverage
import { ReactNode, useMemo } from 'react';
import cn from 'classnames';
import useSWR from 'swr';

import { AnalyticsRes } from 'pages/api/orgs/[id]/analytics';

import { useOrg } from 'lib/context/org';

import { formatRate } from './utils';
import Graph from './graph';

interface LinkProps {
  href: string;
  children: ReactNode;
}

function Link({ href, children }: LinkProps): JSX.Element {
  return (
    <a href={href} target='_blank' rel='noopener noreferrer'>
      {children}
      <style jsx>{`
        a {
          color: var(--accents-5);
          text-decoration: underline;
        }
      `}</style>
    </a>
  );
}

interface CardProps<T> {
  data?: T[];
  title?: ReactNode;
  children?: ReactNode;
  header?: string;
  content?: { dataKey: string; dataLabel: string; rate?: boolean }[];
  color?: string;
}

function Card<T extends Record<string, number> & { week: number }>({
  data,
  title,
  children,
  header,
  content,
  color,
}: CardProps<T>): JSX.Element {
  const num = useMemo(() => {
    if (!data || !content) return undefined;
    const today = data.find((d) => {
      const now = new Date();
      const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate());
      monday.setDate(monday.getDate() - 7 - monday.getDay() + 1);
      return monday.valueOf() === new Date(d.week).valueOf();
    });
    return (today ?? data[data.length - 1])[content[0].dataKey];
  }, [data, content]);

  return (
    <div className='card'>
      <article className='header'>
        <header>
          <h2 className={cn({ loading: num === undefined })}>
            {content && content[0].rate && num !== undefined
              ? formatRate(num)
              : num}
          </h2>
          <h3>{title}</h3>
        </header>
        <p>{children}</p>
      </article>
      {!(data && header && content && color) && (
        <div className='graph loading' />
      )}
      {data && header && content && color && (
        <Graph data={data} header={header} content={content} color={color} />
      )}
      <style jsx>{`
        .card {
          border: 1px solid var(--accents-2);
          border-radius: 8px;
          padding-bottom: 24px;
          max-width: calc(var(--page-width) / 2 - 12px);
          margin: 12px;
        }

        .card:nth-child(odd) {
          margin-left: 0;
        }

        .card:nth-child(even) {
          margin-right: 0;
        }

        .header {
          margin: 24px;
        }

        .header header {
          display: flex;
          align-items: center;
        }

        .header h2 {
          margin: 0;
          font-size: 48px;
          font-weight: 500;
          letter-spacing: -4px;
          line-height: 1;
          height: 48px;
        }

        h2.loading {
          width: 50px;
        }

        .header h3 {
          margin: 0;
          font-size: 16px;
          font-weight: 400;
          margin-left: 12px;
          height: 40px;
        }

        .header p {
          margin: 8px 0;
          font-size: 14px;
          font-weight: 400;
          color: var(--accents-5);
        }

        .loading {
          border-radius: 6px;
        }

        .graph.loading {
          width: calc(488px - 48px);
          height: 250px;
          margin: 0 24px;
        }

        :global(.recharts-cartesian-grid line) {
          shape-rendering: crispedges;
          stroke: var(--accents-2);
        }

        :global(.recharts-active-dot circle) {
          stroke-width: 0;
        }

        :global(.recharts-reference-line-line) {
          shape-rendering: crispedges;
        }

        :global(.recharts-cartesian-grid-horizontal line:first-child) {
          stroke-dasharray: none;
        }

        :global(.xAxis .recharts-cartesian-axis-tick:first-child) {
          display: none;
        }
      `}</style>
    </div>
  );
}

export default function Analytics(): JSX.Element {
  const { org } = useOrg();
  const { data } = useSWR<AnalyticsRes>(
    org?.id ? `/api/orgs/${org.id}/analytics` : null
  );
  const usersWithMeetings = useMemo(
    () =>
      data?.usersWithMeetings.map((d) => ({
        ...d,
        week: new Date(d.week).valueOf(),
      })),
    [data]
  );
  const meetings = useMemo(
    () =>
      data?.meetings.map((d) => ({
        ...d,
        week: new Date(d.week).valueOf(),
      })),
    [data]
  );
  const serviceHours = useMemo(
    () =>
      data?.serviceHours.map((d) => ({
        ...d,
        week: new Date(d.week).valueOf(),
      })),
    [data]
  );
  const users = useMemo(
    () =>
      data?.users.map((d) => ({
        ...d,
        week: new Date(d.week).valueOf(),
      })),
    [data]
  );
  const tutors = useMemo(
    () =>
      data?.tutors.map((d) => ({
        ...d,
        week: new Date(d.week).valueOf(),
      })),
    [data]
  );
  const tutees = useMemo(
    () =>
      data?.tutees.map((d) => ({
        ...d,
        week: new Date(d.week).valueOf(),
      })),
    [data]
  );
  const parents = useMemo(
    () =>
      data?.parents.map((d) => ({
        ...d,
        week: new Date(d.week).valueOf(),
      })),
    [data]
  );

  // TODO: Ensure that the scale on the chart isn't dependent on the data points
  // being equally spaced out. Instead, it should be relative to the data point
  // `date` value.
  // @see {@link http://recharts.org/en-US/api/XAxis#scale}
  return (
    <main>
      <div className='graphs'>
        <Card
          title={
            <>
              Users with meetings
              <br />
              this week
            </>
          }
          data={usersWithMeetings}
          header='Users with meetings'
          content={[
            { dataKey: 'users', dataLabel: 'users with meetings' },
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#64B5F6'
        >
          The number of users who have had a meeting in the last week. This is
          your{' '}
          <Link href='https://thoughtbot.com/blog/north-star-metric'>
            north star metric
          </Link>
          ; it measures how much authentic value you are providing to your
          community.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              users with meetings
            </>
          }
          data={usersWithMeetings}
          header='Users with meetings'
          content={[
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'users', dataLabel: 'users with meetings' },
          ]}
          color='#64B5F6'
        >
          The growth rate of the number of users who have had a meeting in the
          last week. You should always focus on growth rates instead of totals
          to succeed; a growth rate of{' '}
          <Link href='http://www.paulgraham.com/growth.html'>
            5-7% per week
          </Link>{' '}
          is good.
        </Card>
        <Card
          title={
            <>
              Meetings
              <br />
              this week
            </>
          }
          data={meetings}
          header='Meetings'
          content={[
            { dataKey: 'meetings', dataLabel: 'meetings per week' },
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#81C784'
        >
          The number of meetings per week. This is another{' '}
          <Link href='https://kpi.org/KPI-Basics'>
            key performance indicator
          </Link>{' '}
          that is directly correlated with your{' '}
          <Link href='https://thoughtbot.com/blog/north-star-metric'>
            north star metric
          </Link>
          : the number of users with meetings per week.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              meetings per week
            </>
          }
          data={meetings}
          header='Meetings'
          content={[
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'meetings', dataLabel: 'meetings per week' },
          ]}
          color='#81C784'
        >
          The growth rate of the number of meetings per week. This graph should
          look very similar to the growth rate of users with meetings per week;
          both metrics are directly correlated.
        </Card>
        <Card
          title={
            <>
              Service hours
              <br />
              this week
            </>
          }
          data={serviceHours}
          header='Service hours'
          content={[
            { dataKey: 'hours', dataLabel: 'service hours per week' },
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#FF8A65'
        >
          The number of service hours tracked per week.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              service hours per week
            </>
          }
          data={serviceHours}
          header='Service hours'
          content={[
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'hours', dataLabel: 'service hours per week' },
          ]}
          color='#FF8A65'
        >
          The growth rate of the number of service hours tracked per week.
        </Card>
        <Card
          title={
            <>
              New users
              <br />
              this week
            </>
          }
          data={users}
          header='New users'
          content={[
            { dataKey: 'users', dataLabel: 'new users' },
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#F06292'
        >
          The number of users created per week. You’ll notice this graph
          correlates well with the <strong>Total users</strong> graph below it;
          as the number of new users spikes, the total number of users will
          spike too.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              new users per week
            </>
          }
          data={users}
          header='New users'
          content={[
            {
              dataKey: 'growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'users', dataLabel: 'new users' },
          ]}
          color='#F06292'
        >
          The growth rate of the number of users created per week. Note that all
          of these growth rate graphs depict the derivatives of their
          corresponding weekly metrics; they are graphs of the slopes.
        </Card>
        <Card
          title={
            <>
              Users
              <br />
              in all time
            </>
          }
          data={users}
          header='Total users'
          content={[
            { dataKey: 'total', dataLabel: 'total users' },
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#9575CD'
        >
          The total number of users. This is a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; a number that looks good on paper but isn’t action oriented. Use it
          for press releases or marketing, but not to measure actual growth.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              the total number of users
            </>
          }
          data={users}
          header='Total users'
          content={[
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'total', dataLabel: 'total users' },
          ]}
          color='#9575CD'
        >
          The weekly growth rate of the total number of users. Again, this is a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; this growth rate will <i>always</i> be positive and thus will{' '}
          <i>never</i> provide meaningful feedback on how growth is doing.
        </Card>
        <Card
          title={
            <>
              Tutors
              <br />
              in all time
            </>
          }
          data={tutors}
          header='Total tutors'
          content={[
            { dataKey: 'total', dataLabel: 'total tutors' },
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#9575CD'
        >
          The total number of tutors. This is a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; a number that looks good on paper but isn’t action oriented. Use it
          for press releases or marketing, but not to measure actual growth.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              the total number of tutors
            </>
          }
          data={tutors}
          header='Total tutors'
          content={[
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'total', dataLabel: 'total tutors' },
          ]}
          color='#9575CD'
        >
          The weekly growth rate of the total number of tutors. Again, this is a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; this growth rate will <i>always</i> be positive and thus will{' '}
          <i>never</i> provide meaningful feedback on how growth is doing.
        </Card>
        <Card
          title={
            <>
              Tutees
              <br />
              in all time
            </>
          }
          data={tutees}
          header='Total tutees'
          content={[
            { dataKey: 'total', dataLabel: 'total tutees' },
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#9575CD'
        >
          The total number of tutees. This is a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; a number that looks good on paper but isn’t action oriented. Use it
          for press releases or marketing, but not to measure actual growth.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              the total number of tutees
            </>
          }
          data={tutees}
          header='Total tutees'
          content={[
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'total', dataLabel: 'total tutees' },
          ]}
          color='#9575CD'
        >
          The weekly growth rate of the total number of tutees. Again, this is a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; this growth rate will <i>always</i> be positive and thus will{' '}
          <i>never</i> provide meaningful feedback on how growth is doing.
        </Card>
        <Card
          title={
            <>
              Parents
              <br />
              in all time
            </>
          }
          data={parents}
          header='Total parents'
          content={[
            { dataKey: 'total', dataLabel: 'total parents' },
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
          ]}
          color='#9575CD'
        >
          The total number of parents. This is a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; a number that looks good on paper but isn’t action oriented. Use it
          for press releases or marketing, but not to measure actual growth.
        </Card>
        <Card
          title={
            <>
              Weekly growth rate of
              <br />
              the total number of parents
            </>
          }
          data={parents}
          header='Total parents'
          content={[
            {
              dataKey: 'total_growth',
              dataLabel: 'from previous week',
              rate: true,
            },
            { dataKey: 'total', dataLabel: 'total parents' },
          ]}
          color='#9575CD'
        >
          The weekly growth rate of the total number of parents. Again, this is
          a{' '}
          <Link href='https://hbr.org/2010/02/entrepreneurs-beware-of-vanity-metrics'>
            vanity metric
          </Link>
          ; this growth rate will <i>always</i> be positive and thus will{' '}
          <i>never</i> provide meaningful feedback on how growth is doing.
        </Card>
      </div>
      <style jsx>{`
        main {
          max-width: var(--page-width-with-margin);
          padding: 48px 24px;
          margin: 0 auto;
        }

        .graphs {
          display: flex;
          flex-wrap: wrap;
        }
      `}</style>
    </main>
  );
}