Skip to content

Commit

Permalink
add initial insights dashboard
Browse files Browse the repository at this point in the history
  • Loading branch information
xvvvyz committed Sep 1, 2023
1 parent a1cb6bc commit c16449c
Show file tree
Hide file tree
Showing 24 changed files with 798 additions and 171 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const Catch = () => null;

export default Catch;
34 changes: 34 additions & 0 deletions app/(account)/subjects/[subjectId]/@insights/insights/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use client';

import Charts from '@/(account)/subjects/[subjectId]/timeline/_components/charts';
import IconButton from '@/_components/icon-button';
import { Dialog } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { useRouter } from 'next/router';

interface PageProps {
subjectId: string;
}

const Page = ({ subjectId }: PageProps) => {
const router = useRouter();

return (
<Dialog className="relative z-10" onClose={() => router.back()} open>
<div className="fixed inset-0 overflow-y-auto">
<Dialog.Panel className="min-h-full w-full space-y-24 bg-bg-1 p-8 md:p-16 lg:p-24">
<div className="flex items-center justify-between">
<h1 className="text-2xl">Insights</h1>
<IconButton
icon={<XMarkIcon className="relative -right-[0.16em] w-7" />}
onClick={() => router.back()}
/>
</div>
<Charts subjectId={subjectId} />
</Dialog.Panel>
</div>
</Dialog>
);
};

export default Page;
Original file line number Diff line number Diff line change
Expand Up @@ -514,18 +514,16 @@ const SessionForm = ({
</SortableContext>
</DndContext>
</ul>
<div className="form mt-4">
<Button
className="w-full"
colorScheme="transparent"
onClick={() =>
modulesArray.append({ content: '', inputs: [] } as any)
}
>
<PlusIcon className="w-5" />
Add module
</Button>
</div>
<Button
className="mt-4 w-full"
colorScheme="transparent"
onClick={() =>
modulesArray.append({ content: '', inputs: [] } as any)
}
>
<PlusIcon className="w-5" />
Add module
</Button>
<div className="form mt-4 flex-row gap-4">
{draft && (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const Page = async ({ params: { subjectId } }: PageProps) => {
if (!eventTypes.length && !isTeamMember) return null;

return (
<div className="px-4">
<div>
{!!eventTypes.length && (
<ul
className={twMerge(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,33 +44,29 @@ const TimelineEvents = ({
);
}, [events]);

return (
<div className="space-y-4 px-4">
{formattedEvents.map((groups) => {
const dayGroup = Array.from(groups.values());
const firstEvent = dayGroup[0][0];
return formattedEvents.map((groups) => {
const dayGroup = Array.from(groups.values());
const firstEvent = dayGroup[0][0];

return (
<div className="space-y-4" key={firstEvent.created_at}>
<DateTime
className="smallcaps mx-4 flex h-16 items-end justify-end border-l-2 border-dashed border-alpha-2"
date={firstEvent.created_at}
formatter="date"
/>
{dayGroup.map((eventGroup) => (
<TimelineEventCard
group={eventGroup}
isTeamMember={isTeamMember}
key={eventGroup[0].id}
subjectId={subjectId}
userId={userId}
/>
))}
</div>
);
})}
</div>
);
return (
<div className="space-y-4" key={firstEvent.created_at}>
<DateTime
className="smallcaps mx-4 flex h-14 items-end justify-end border-l-2 border-dashed border-alpha-2"
date={firstEvent.created_at}
formatter="date"
/>
{dayGroup.map((eventGroup) => (
<TimelineEventCard
group={eventGroup}
isTeamMember={isTeamMember}
key={eventGroup[0].id}
subjectId={subjectId}
userId={userId}
/>
))}
</div>
);
});
};

export default TimelineEvents;

This file was deleted.

45 changes: 17 additions & 28 deletions app/(account)/subjects/[subjectId]/timeline/@events/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import DownloadEventsButton from '@/(account)/subjects/[subjectId]/timeline/@events/_components/download-events-button';
import TimelineEvents from '@/(account)/subjects/[subjectId]/timeline/@events/_components/timeline-events';
import Empty from '@/_components/empty';
import Header from '@/_components/header';
import getCurrentTeamId from '@/_server/get-current-team-id';
import getCurrentUser from '@/_server/get-current-user';
import getSubject from '@/_server/get-subject';
import listEvents, { ListEventsData } from '@/_server/list-events';

import { InformationCircleIcon } from '@heroicons/react/24/outline';

interface PageProps {
Expand All @@ -25,33 +22,25 @@ const Page = async ({ params: { subjectId } }: PageProps) => {
],
);

if (!subject || !user) return null;
if (!subject || !user || !events?.length) {
return (
<>
<div className="mx-4 h-16 border-l-2 border-dashed border-alpha-2" />
<Empty>
<InformationCircleIcon className="w-7" />
Recorded events will appear here.
</Empty>
</>
);
}

return (
<>
<Header className="mb-2">
<h1 className="text-2xl">Timeline</h1>
<DownloadEventsButton disabled={!events?.length} subjectId={subjectId}>
Download events
</DownloadEventsButton>
</Header>
{events?.length ? (
<TimelineEvents
events={events as ListEventsData}
isTeamMember={subject.team_id === teamId}
subjectId={subjectId}
userId={user.id}
/>
) : (
<div className="space-y-4 px-4">
<div className="mx-4 h-16 border-l-2 border-dashed border-alpha-2" />
<Empty>
<InformationCircleIcon className="w-7" />
Recorded events will appear here.
</Empty>
</div>
)}
</>
<TimelineEvents
events={events as ListEventsData}
isTeamMember={subject.team_id === teamId}
subjectId={subjectId}
userId={user.id}
/>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const Page = async ({ params: { subjectId } }: PageProps) => {
if (!listItems.length && !isTeamMember) return null;

return (
<div className="px-4">
<div>
{!!listItems.length && (
<ul
className={twMerge(
Expand Down
49 changes: 49 additions & 0 deletions app/(account)/subjects/[subjectId]/timeline/_components/charts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use client';

import EventCounts from '@/(account)/subjects/[subjectId]/timeline/_components/event-counts';
import EventsByTimeOfDay from '@/(account)/subjects/[subjectId]/timeline/_components/events-by-time-of-day';
import EventsOverTime from '@/(account)/subjects/[subjectId]/timeline/_components/events-over-time';
import Spinner from '@/_components/spinner';
import { useParentSize } from '@cutting/use-get-parent-size';
import { useToggle } from '@uidotdev/usehooks';
import { useEffect, useMemo, useRef, useState } from 'react';

interface ChartsProps {
subjectId: string;
}

const Charts = ({ subjectId }: ChartsProps) => {
const [data, setData] = useState<Array<Record<string, unknown>>>();
const [isLoading, toggleIsLoading] = useToggle(true);
const ref = useRef<HTMLDivElement>(null);
const { width } = useParentSize(ref);

useEffect(() => {
(async () => {
const res = await fetch(`/subjects/${subjectId}/events.json`);
setData(await res.json());
toggleIsLoading(false);
})();
}, [setData, subjectId, toggleIsLoading]);

const events = useMemo(
() =>
data?.filter(
(d) =>
typeof d['Session number'] === 'undefined' ||
d['Module number'] === 1,
),
[data],
);

return (
<div className="smallcaps flex flex-col items-center gap-8" ref={ref}>
{isLoading && <Spinner loadingText="Loading data…" />}
<EventCounts events={events} width={width} />
<EventsOverTime events={events} width={width} />
<EventsByTimeOfDay events={events} width={width} />
</div>
);
};

export default Charts;
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,18 @@
import Button from '@/_components/button';
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline';
import { useToggle } from '@uidotdev/usehooks';
import { ReactNode } from 'react';

interface DownloadEventsButtonProps {
children: ReactNode;
className?: string;
disabled?: boolean;
subjectId: string;
}

const DownloadEventsButton = ({
children,
className,
disabled,
subjectId,
}: DownloadEventsButtonProps) => {
const DownloadEventsButton = ({ subjectId }: DownloadEventsButtonProps) => {
const [isDownloading, toggleIsDownloading] = useToggle(false);

return (
<Button
colorScheme="transparent"
className={className}
disabled={disabled || isDownloading}
disabled={isDownloading}
onClick={async () => {
toggleIsDownloading();
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
Expand All @@ -40,7 +30,7 @@ const DownloadEventsButton = ({
size="sm"
>
<ArrowDownTrayIcon className="w-5" />
{children}
CSV
</Button>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client';

import PlotFigure from '@/(account)/subjects/[subjectId]/timeline/_components/plot-figure';
import { axisY, barX, groupY } from '@observablehq/plot';

interface EventCountsProps {
events?: Array<Record<string, unknown>>;
width?: number;
}

const EventCounts = ({ events = [], width }: EventCountsProps) => {
if (!events.length) return null;

return (
<PlotFigure
options={{
marks: [
axisY({
label: null,
lineWidth: 9,
textOverflow: 'ellipsis',
tickSize: 0,
}),
barX(
events,
groupY(
{ x: 'count' },
{ sort: { reverse: true, y: 'x' }, tip: true, y: 'Name' },
),
),
],
title: 'Event counts',
width,
x: { label: 'Count', tickFormat: (d) => (d % 1 ? null : d) },
y: { padding: 0.05 },
}}
/>
);
};

export default EventCounts;
Loading

0 comments on commit c16449c

Please sign in to comment.