Skip to content

Commit 477ca84

Browse files
chore: refactor ranges to prepare for custom ranges
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 4ad71de commit 477ca84

File tree

11 files changed

+374
-77
lines changed

11 files changed

+374
-77
lines changed

Cargo.lock

Lines changed: 283 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/bun.lockb

0 Bytes
Binary file not shown.

web/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"date-fns": "^4.1.0",
2525
"fets": "^0.8.3",
2626
"fuzzysort": "^3.1.0",
27-
"lightningcss": "^1.27.0",
27+
"lightningcss": "^1.28.1",
2828
"lucide-react": "^0.454.0",
2929
"react": "^18.3.1",
3030
"react-dom": "^18.3.1",
@@ -37,7 +37,7 @@
3737
"@types/react": "^18.3.12",
3838
"@types/react-dom": "^18.3.1",
3939
"@types/react-simple-maps": "^3.0.6",
40-
"astro": "^4.16.8",
40+
"astro": "^4.16.9",
4141
"bun-types": "^1.1.34",
4242
"rollup-plugin-license": "^3.5.3",
4343
"typescript": "^5.6.3"

web/src/api/hooks.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { DateRange, Dimension, DimensionFilter, DimensionTableRow, Metric,
55

66
import { api } from ".";
77
import { queryClient, useQuery } from "./query";
8-
import { resolveRange, type RangeName } from "./ranges";
8+
import { rangeDataPoints, rangeEndsToday, type RangeName } from "./ranges";
99

1010
export const useMe = () => {
1111
const { data, isLoading } = useQuery({
@@ -96,23 +96,23 @@ export const useDimension = ({
9696
export const useProjectData = ({
9797
project,
9898
metric,
99-
rangeName = "last7Days",
99+
range,
100100
filters = [],
101101
}: {
102102
project?: ProjectResponse;
103103
metric: Metric;
104-
rangeName?: RangeName;
104+
range: DateRange;
105105
filters?: DimensionFilter[];
106106
}) => {
107-
const { range, graphRange, dataPoints } = useMemo(() => resolveRange(rangeName), [rangeName]);
108-
109107
let refetchInterval = undefined;
110108
let staleTime = 1000 * 60 * 10;
111-
if (rangeName === "today" || rangeName.startsWith("last")) {
109+
if (rangeEndsToday(range)) {
112110
refetchInterval = 1000 * 60;
113111
staleTime = 0;
114112
}
115113

114+
const dataPoints = rangeDataPoints(range);
115+
116116
const {
117117
data: stats,
118118
isError: isErrorStats,
@@ -138,7 +138,7 @@ export const useProjectData = ({
138138
refetchInterval,
139139
staleTime,
140140
enabled: project !== undefined,
141-
queryKey: ["project_graph", project?.id, range, graphRange, metric, filters, dataPoints],
141+
queryKey: ["project_graph", project?.id, range, metric, filters, dataPoints],
142142
queryFn: () =>
143143
api["/api/dashboard/project/{project_id}/graph"]
144144
.post({ json: { range, metric, dataPoints, filters }, params: { project_id: project?.id ?? "" } })
@@ -155,7 +155,7 @@ export const useProjectData = ({
155155
graph: {
156156
error: isErrorGraph,
157157
loading: isLoadingGraph,
158-
range: graphRange,
158+
range,
159159
data: graph?.data ? toDataPoints(graph.data, range, metric) : [],
160160
},
161161
isLoading: isLoadingStats || isLoadingGraph,

web/src/api/ranges.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,50 @@ import {
1515
import type { DateRange } from "./types";
1616
import type { GraphRange } from "../components/graph/graph";
1717

18+
export const rangeEndsToday = (range: DateRange) => {
19+
const now = new Date().getTime();
20+
return startOfDay(now) === startOfDay(range.end);
21+
};
22+
23+
export const rangeGraphRange = (range: DateRange): GraphRange => {
24+
const days = differenceInDays(range.end, range.start);
25+
if (days < 2) {
26+
return "hour";
27+
}
28+
const months = differenceInMonths(range.end, range.start);
29+
if (months < 2) {
30+
return "day";
31+
}
32+
return "month";
33+
};
34+
35+
export const rangeDataPoints = (range: DateRange): number => {
36+
switch (rangeGraphRange(range)) {
37+
case "hour":
38+
return differenceInHours(range.end, range.start) + 1;
39+
case "day":
40+
return differenceInDays(range.end, range.start) + 1;
41+
case "month":
42+
return differenceInMonths(range.end, range.start) + 1;
43+
}
44+
throw new Error("unreachable");
45+
};
46+
47+
export const serializeRange = (range: DateRange): string => {
48+
const start = new Date(range.start);
49+
const end = new Date(range.end);
50+
return `${Number(start)}:${Number(end)}`;
51+
};
52+
53+
export const deserializeRange = (range: string): DateRange => {
54+
if (!range.includes(":")) {
55+
return ranges[range as RangeName]().range;
56+
}
57+
58+
const [start, end] = range.split(":").map(Number);
59+
return { start, end };
60+
};
61+
1862
export const rangeNames = {
1963
today: "Today",
2064
yesterday: "Yesterday",
@@ -32,8 +76,6 @@ const lastXDays = (days: number) => {
3276
return { start, end };
3377
};
3478

35-
export const resolveRange = (name: RangeName) => ranges[name]();
36-
3779
// all rangeNames are keys of the ranges object
3880
export const ranges: Record<RangeName, () => { range: DateRange; dataPoints: number; graphRange: GraphRange }> = {
3981
today: () => {

web/src/components/dimensions/index.tsx

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as Tabs from "@radix-ui/react-tabs";
2-
import { ArrowDownIcon, LinkIcon, PinIcon, SquareArrowOutUpRightIcon } from "lucide-react";
2+
import { LinkIcon, PinIcon, SquareArrowOutUpRightIcon } from "lucide-react";
33
import styles from "./dimensions.module.css";
44

55
import { type Dimension, type DimensionTableRow, dimensionNames, metricNames, useDimension } from "../../api";
@@ -92,11 +92,6 @@ export const DimensionDropdown = ({
9292
</option>
9393
))}
9494
</select>
95-
{/* {Object.entries(dimensions).map(([key, value]) => (
96-
<Tabs.Trigger key={key} value={value}>
97-
{dimensionNames[value]}
98-
</Tabs.Trigger>
99-
))} */}
10095
<div>{metricNames[query.metric]}</div>
10196
</Tabs.List>
10297
{dimensions.map((dimension) => (

web/src/components/graph/graph.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { addMonths } from "date-fns";
66
import type { DataPoint } from ".";
77
import { formatMetricVal } from "../../utils";
88
import { useWindowSize } from "@uidotdev/usehooks";
9+
import type { DateRange } from "../../api";
10+
import { rangeGraphRange } from "../../api/ranges";
911

1012
export type GraphRange = "year" | "month" | "day" | "hour";
1113

@@ -39,12 +41,13 @@ const Tooltip = (props: SliceTooltipProps & { title: string; range: GraphRange }
3941
export const LineGraph = ({
4042
data,
4143
title,
42-
range = "day",
44+
range,
4345
}: {
4446
data: DataPoint[];
4547
title: string;
46-
range?: GraphRange;
48+
range: DateRange;
4749
}) => {
50+
const graphRange = rangeGraphRange(range);
4851
const max = useMemo(() => Math.max(...data.map((d) => d.y)), [data]);
4952
const yCount = 5;
5053

@@ -76,7 +79,7 @@ export const LineGraph = ({
7679
axisRight={null}
7780
axisBottom={{
7881
legend: "",
79-
format: (value: Date) => formatDate(value, range),
82+
format: (value: Date) => formatDate(value, graphRange),
8083
tickValues: xCount,
8184
}}
8285
axisLeft={{
@@ -90,7 +93,7 @@ export const LineGraph = ({
9093
pointLabel="data.yFormatted"
9194
pointLabelYOffset={-12}
9295
enableSlices="x"
93-
sliceTooltip={(props) => <Tooltip {...props} title={title} range={range} />}
96+
sliceTooltip={(props) => <Tooltip {...props} title={title} range={graphRange} />}
9497
defs={[
9598
{
9699
colors: [

web/src/components/project.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import styles from "./project.module.css";
22
import _map from "./worldmap.module.css";
33

4-
import { Suspense, lazy, useEffect, useState } from "react";
4+
import { Suspense, lazy, useEffect, useMemo, useState } from "react";
55
import { useLocalStorage } from "@uidotdev/usehooks";
66

7-
import { type RangeName, resolveRange } from "../api/ranges";
7+
import { deserializeRange, type RangeName } from "../api/ranges";
88
import { metricNames, useDimension, useProject, useProjectData } from "../api";
99
import type { DimensionFilter, DateRange, Metric, ProjectResponse, DimensionTableRow, Dimension } from "../api";
1010

@@ -28,17 +28,17 @@ export type ProjectQuery = {
2828
export const Project = () => {
2929
const [projectId, setProjectId] = useState<string | undefined>();
3030
const [filters, setFilters] = useState<DimensionFilter[]>([]);
31-
const [dateRange, setDateRange] = useLocalStorage<RangeName>("date-range", "last7Days");
3231
const [metric, setMetric] = useLocalStorage<Metric>("metric", "views");
32+
const [rangeString, setRangeString] = useLocalStorage<string>("date-range", "last7Days");
33+
const range = useMemo(() => deserializeRange(rangeString), [rangeString]);
3334

3435
useEffect(() => {
3536
if (typeof window === "undefined") return;
3637
setProjectId(window?.document.location.pathname.split("/").pop());
3738
}, []);
3839

3940
const { project } = useProject(projectId);
40-
const { graph, stats } = useProjectData({ project, metric, rangeName: dateRange, filters });
41-
const { range } = resolveRange(dateRange);
41+
const { graph, stats } = useProjectData({ project, metric, range, filters });
4242
if (!project) return null;
4343

4444
const query = { metric, range, filters, project };
@@ -92,7 +92,7 @@ export const Project = () => {
9292
<div>
9393
<div className={styles.projectHeader}>
9494
<ProjectHeader project={project} stats={stats.data} />
95-
<SelectRange onSelect={(name: RangeName) => setDateRange(name)} range={dateRange} />
95+
<SelectRange onSelect={(name) => setRangeString(name)} range={rangeString} />
9696
</div>
9797
<SelectMetrics data={stats.data} metric={metric} setMetric={setMetric} className={styles.projectStats} />
9898
<SelectFilters value={filters} onChange={setFilters} />

web/src/components/project/range.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@ import { useRef } from "react";
33
import { rangeNames, type RangeName } from "../../api/ranges";
44
import { cls } from "../../utils";
55

6-
export const SelectRange = ({ onSelect, range }: { onSelect: (name: RangeName) => void; range: RangeName }) => {
6+
export const SelectRange = ({ onSelect, range }: { onSelect: (name: string) => void; range: string }) => {
7+
const rangeName = range.includes(":") ? "Custom" : rangeNames[range as RangeName];
78
const detailsRef = useRef<HTMLDetailsElement>(null);
89

9-
const handleSelect = (name: RangeName) => () => {
10+
const handleSelect = (name: string) => () => {
1011
if (detailsRef.current) detailsRef.current.open = false;
1112
onSelect(name);
1213
};
1314

1415
return (
1516
<details ref={detailsRef} className={cls("dropdown", styles.selectRange)}>
16-
<summary>{rangeNames[range]}</summary>
17+
<summary>{rangeName}</summary>
1718
<ul>
1819
{Object.entries(rangeNames).map(([key, value]) => (
1920
<li key={key}>

web/src/components/projects.tsx

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import { Suspense } from "react";
1+
import { Suspense, useMemo } from "react";
22
import styles from "./projects.module.css";
33

44
import { useLocalStorage } from "@uidotdev/usehooks";
55
import { ChevronDownIcon } from "lucide-react";
66

7-
import { type Metric, type ProjectResponse, api, metricNames, useMe, useProjectData, useQuery } from "../api";
8-
import type { RangeName } from "../api/ranges";
7+
import {
8+
type DateRange,
9+
type Metric,
10+
type ProjectResponse,
11+
api,
12+
metricNames,
13+
useMe,
14+
useProjectData,
15+
useQuery,
16+
} from "../api";
17+
import { deserializeRange, type RangeName } from "../api/ranges";
918
import { getUsername } from "../utils";
1019
import { LineGraph } from "./graph";
1120
import { SelectRange } from "./project/range";
@@ -45,9 +54,10 @@ export const Projects = () => {
4554
queryFn: () => api["/api/dashboard/projects"].get().json(),
4655
});
4756

48-
const [dateRange, setDateRange] = useLocalStorage<RangeName>("date-range", "last7Days");
4957
const [metric, setMetric] = useLocalStorage<Metric>("metric", "views");
5058
const [hiddenProjects, setHiddenProjects] = useLocalStorage<string[]>("hiddenProjects", []);
59+
const [rangeString, setRangeString] = useLocalStorage<string>("date-range", "last7Days");
60+
const range = useMemo(() => deserializeRange(rangeString), [rangeString]);
5161

5262
const projects = data?.projects || [];
5363

@@ -75,7 +85,7 @@ export const Projects = () => {
7585
<div className={styles.projects}>
7686
<div className={styles.header}>
7787
<h1>Dashboard</h1>
78-
<SelectRange onSelect={(name: RangeName) => setDateRange(name)} range={dateRange} />
88+
<SelectRange onSelect={(name) => setRangeString(name)} range={rangeString} />
7989
</div>
8090

8191
<Suspense>
@@ -89,7 +99,7 @@ export const Projects = () => {
8999
>
90100
{projects.map((project) => (
91101
<Accordion.Item key={project.id} value={project.id}>
92-
<Project project={project} metric={metric} setMetric={setMetric} rangeName={dateRange} />
102+
<Project project={project} metric={metric} setMetric={setMetric} range={range} />
93103
</Accordion.Item>
94104
))}
95105
</Accordion.Root>
@@ -102,9 +112,9 @@ const Project = ({
102112
project,
103113
metric,
104114
setMetric,
105-
rangeName,
106-
}: { project: ProjectResponse; metric: Metric; setMetric: (value: Metric) => void; rangeName: RangeName }) => {
107-
const { stats, graph, isLoading, isError } = useProjectData({ project, metric, rangeName });
115+
range,
116+
}: { project: ProjectResponse; metric: Metric; setMetric: (value: Metric) => void; range: DateRange }) => {
117+
const { stats, graph, isLoading, isError } = useProjectData({ project, metric, range });
108118

109119
return (
110120
<article className={styles.project} data-loading={isLoading || isError} data-error={isError}>

0 commit comments

Comments
 (0)