Skip to content

Commit

Permalink
feat: finish admin panel user & rec page
Browse files Browse the repository at this point in the history
  • Loading branch information
swh00tw committed Mar 4, 2024
1 parent fc08cff commit eb25320
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 183 deletions.
3 changes: 2 additions & 1 deletion apps/recnet/src/app/admin/stats/StatBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ export function StatBox({
"p-6",
"gap-y-3",
"w-fit",
"relative",
className
)}
>
<div
className={cn(
"flex flex-row gap-x-1 text-gray-11 text-[14px] font-medium items-center"
"flex flex-row gap-x-1 text-gray-11 text-[14px] font-medium items-center sticky left-0 top-0 bg-white z-10"
)}
>
{icon}
Expand Down
163 changes: 114 additions & 49 deletions apps/recnet/src/app/admin/stats/user-rec/RecsCycleBarChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import { ParentSize } from "@visx/responsive";
import { Bar } from "@visx/shape";
import { Group } from "@visx/group";
import { AxisBottom } from "@visx/axis";
import { useTooltip, useTooltipInPortal, defaultStyles } from "@visx/tooltip";
import { localPoint } from "@visx/event";
import { WeekTs, formatDate } from "@/utils/date";
import { Text } from "@radix-ui/themes";

type Timestamp = string;

Expand All @@ -15,17 +19,45 @@ interface RecsCycleBarChartProps {
data: Record<Timestamp, number>;
}

interface TooltipData {
ts: number;
count: number;
}

const themeColor = "#2A78D0";
const tooltipStyles = {
...defaultStyles,
minWidth: 60,
backgroundColor: "black",
color: "white",
};
let tooltipTimeout: number;

function BarChart(props: RecsCycleBarChartProps) {
const { parentWidth, parentHeight, data } = props;
const {
tooltipOpen,
tooltipLeft,
tooltipTop,
tooltipData,
hideTooltip,
showTooltip,
} = useTooltip<TooltipData>();

const { containerRef, TooltipInPortal } = useTooltipInPortal({
// TooltipInPortal is rendered in a separate child of <body /> and positioned
// with page coordinates which should be updated on scroll. consider using
// Tooltip or TooltipWithBounds if you don't need to render inside a Portal
scroll: true,
});
// data
const timestamps = Object.keys(data).map((key) => parseInt(key, 10));

// bounds
const margin = { top: 40, right: 0, bottom: 40, left: 0 };
const xMax = parentWidth;
const yMax = parentHeight - margin.top - margin.bottom;

// data
const timestamps = Object.keys(data).map((key) => parseInt(key, 10));
const barWidth = parentWidth / timestamps.length - 5;

const xScale = useMemo(() => {
return scaleUtc({
Expand All @@ -45,52 +77,85 @@ function BarChart(props: RecsCycleBarChartProps) {
}, [yMax, data]);

return (
<svg width={parentWidth} height={parentHeight}>
<Group top={margin.top}>
{Object.keys(data)
.map((key) => {
const ts = parseInt(key, 10);
return {
ts,
count: data[key],
};
})
.map((d) => {
const barWidth = 20;
const barHeight = yMax - (yScale(d.count) ?? 0);
const barX = xScale(new Date(d.ts));
const barY = yMax - barHeight;
return (
<Bar
key={`bar-${d.ts}`}
x={barX}
y={barY}
width={barWidth}
height={barHeight}
fill="#0091FF"
onClick={() => {}}
/>
);
})}
</Group>
<AxisBottom
top={yMax + margin.bottom}
scale={xScale}
tickFormat={(d) => {
if (d instanceof Date) {
return xScale.tickFormat(undefined, "%b %d")(d);
}
return "";
}}
stroke={themeColor}
tickStroke={themeColor}
tickLabelProps={{
fill: themeColor,
fontSize: 9,
textAnchor: "middle",
}}
/>
</svg>
<>
<svg width={parentWidth} height={parentHeight} ref={containerRef}>
<Group top={margin.top}>
{Object.keys(data)
.map((key) => {
const ts = parseInt(key, 10);
return {
ts,
count: data[key],
};
})
.map((d) => {
const barHeight = yMax - (yScale(d.count) ?? 0);
const barX = xScale(new Date(d.ts));
const barY = yMax - barHeight;
return (
<Bar
key={`bar-${d.ts}`}
x={barX}
y={barY}
width={barWidth}
height={barHeight}
fill="#0091FF"
onClick={() => {}}
onMouseLeave={() => {
tooltipTimeout = window.setTimeout(() => {
hideTooltip();
}, 300);
}}
onMouseMove={(event) => {
if (tooltipTimeout) clearTimeout(tooltipTimeout);
// TooltipInPortal expects coordinates to be relative to containerRef
// localPoint returns coordinates relative to the nearest SVG, which
// is what containerRef is set to.
const eventSvgCoords = localPoint(event);
const left = barX + barWidth / 2;
showTooltip({
tooltipData: {
ts: d.ts,
count: d.count,
},
tooltipTop: eventSvgCoords?.y,
tooltipLeft: left,
});
}}
/>
);
})}
</Group>
<AxisBottom
top={yMax + margin.bottom}
scale={xScale}
tickFormat={(d) => {
if (d instanceof Date) {
return xScale.tickFormat(undefined, "%b %d")(d);
}
return "";
}}
stroke={themeColor}
tickStroke={themeColor}
tickLabelProps={{
fill: themeColor,
fontSize: 9,
textAnchor: "middle",
}}
/>
</svg>
{tooltipData && tooltipOpen ? (
<TooltipInPortal
top={tooltipTop}
left={tooltipLeft}
style={tooltipStyles}
className="flex flex-col gap-y-2 p-2 rounded-8"
>
<Text className="text-[14px] text-blue-7 py-1 font-medium">{`${formatDate(new Date(tooltipData.ts - WeekTs))} ~ ${formatDate(new Date(tooltipData.ts))}`}</Text>
<div>Num of Rec: {tooltipData.count}</div>
</TooltipInPortal>
) : null}
</>
);
}

Expand Down
15 changes: 9 additions & 6 deletions apps/recnet/src/app/admin/stats/user-rec/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { db } from "@/firebase/admin";
import { StatBox, StatBoxSkeleton } from "@/app/admin/stats/StatBox";
import { Pencil1Icon, PersonIcon } from "@radix-ui/react-icons";
import { getDateFromFirebaseTimestamp, getLatestCutOff } from "@/utils/date";
import { getDateFromFirebaseTimestamp, getNextCutOff } from "@/utils/date";
import { Timestamp } from "firebase-admin/firestore";
import { withSuspense } from "@/utils/withSuspense";
import groupBy from "lodash.groupby";
Expand Down Expand Up @@ -37,7 +37,8 @@ const RecCount = withSuspense(

const RecsThisCycle = withSuspense(
async () => {
const cutOff = getLatestCutOff();
const cutOff = getNextCutOff();
console.log("cutOff", cutOff);
const recsThisCycle = await db
.collection("recommendations")
.where("cutoff", "==", Timestamp.fromMillis(cutOff.getTime()))
Expand All @@ -62,7 +63,7 @@ const RecsBarChart = withSuspense(
if (res.success) {
return res.data;
} else {
console.error("Failed to parse rec", res.error);
// console.error("Failed to parse rec", res.error);
return null;
}
})
Expand Down Expand Up @@ -93,9 +94,11 @@ const RecsBarChart = withSuspense(
<StatBox
title="Number of Recs by Cutoff Date"
icon={<Pencil1Icon />}
className="h-[300px] w-[40%] min-w-[500px]"
className="h-[300px] w-[100%] md:w-[40%] overflow-x-auto whitespace-nowrap overflow-y-hidden"
>
<RecsCycleBarChart data={recCountByCycle} />
<div className="min-w-[400px] h-full">
<RecsCycleBarChart data={recCountByCycle} />
</div>
</StatBox>
);
},
Expand All @@ -105,7 +108,7 @@ const RecsBarChart = withSuspense(
export default async function UserRecStats() {
return (
<div className="flex flex-col gap-y-4">
<div className="flex flex-row gap-x-4">
<div className="flex flex-row gap-4 flex-wrap">
<CurrentUserCount />
<RecCount />
<RecsThisCycle />
Expand Down
1 change: 1 addition & 0 deletions apps/recnet/src/utils/date.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FirebaseTs } from "@/types/rec";
import { Timestamp } from "firebase/firestore";

const CYCLE_DUE_DAY = 2;
export const WeekTs = 604800000 as const;
export const START_DATE = new Date(2023, 9, 24);
export const Months = [
"Jan",
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
"@react-email/components": "0.0.14",
"@react-email/render": "^0.0.12",
"@visx/axis": "^3.8.0",
"@visx/event": "^3.3.0",
"@visx/group": "^3.3.0",
"@visx/responsive": "^3.3.0",
"@visx/scale": "^3.5.0",
"@visx/shape": "^3.5.0",
"@visx/tooltip": "^3.3.0",
"@visx/vendor": "^3.5.0",
"chance": "^1.1.11",
"clsx": "^2.1.0",
Expand Down
Loading

0 comments on commit eb25320

Please sign in to comment.