Skip to content

Commit 6e2b982

Browse files
feat: basic show dimension details
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 757fff0 commit 6e2b982

File tree

11 files changed

+146
-20
lines changed

11 files changed

+146
-20
lines changed

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@
1212
"defaultBranch": "main"
1313
},
1414
"files": {
15-
"ignore": ["**/node_modules/*"]
15+
"ignore": ["**/node_modules/*", "**/api/dashboard.ts"]
1616
}
1717
}

web/src/components/dialog.module.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
position: fixed;
2424
inset: 0;
2525
animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
26+
z-index: 100;
2627
}
2728

2829
.title {
@@ -31,6 +32,7 @@
3132

3233
.content {
3334
position: fixed;
35+
z-index: 100;
3436
top: 50%;
3537
left: 50%;
3638
transform: translate(-50%, -50%);
@@ -40,4 +42,21 @@
4042
padding: 1rem;
4143
padding-bottom: 0.5rem;
4244
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
45+
overflow-y: auto;
46+
}
47+
48+
.close {
49+
position: fixed;
50+
z-index: 1000;
51+
background-color: white;
52+
border: none;
53+
right: 0;
54+
margin: 1rem;
55+
pointer-events: auto;
56+
border-radius: 1rem;
57+
display: flex;
58+
width: 2.5rem;
59+
height: 2.5rem;
60+
justify-content: center;
61+
align-items: center;
4362
}

web/src/components/dialog.tsx

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import * as Dia from "@radix-ui/react-dialog";
2+
import { XIcon } from "lucide-react";
3+
import { cls } from "../utils";
24
import styles from "./dialog.module.css";
35

46
export const Dialog = ({
@@ -8,23 +10,38 @@ export const Dialog = ({
810
trigger,
911
children,
1012
onOpenChange,
13+
className,
14+
showClose,
15+
hideTitle,
1116
}: {
12-
title?: string;
17+
title: string;
1318
description?: string;
1419
hideDescription?: boolean;
15-
trigger: React.ReactNode;
20+
trigger: React.ReactNode | (() => React.ReactNode);
1621
children: React.ReactNode;
1722
onOpenChange?: (open: boolean) => void;
23+
className?: string;
24+
showClose?: boolean;
25+
hideTitle?: boolean;
1826
}) => {
1927
return (
2028
<Dia.Root onOpenChange={onOpenChange}>
21-
<Dia.Trigger asChild>{trigger}</Dia.Trigger>
29+
<Dia.Trigger asChild>{typeof trigger === "function" ? trigger() : trigger}</Dia.Trigger>
2230
<Dia.Portal>
2331
<Dia.Overlay className={styles.overlay} />
32+
{showClose && (
33+
<Dia.Close asChild>
34+
<button type="button" className={styles.close}>
35+
<XIcon size="24" />
36+
</button>
37+
</Dia.Close>
38+
)}
2439

2540
<Dia.Content asChild>
26-
<article className={styles.content}>
27-
{title && <Dia.Title className={styles.title}>{title}</Dia.Title>}
41+
<article className={cls(styles.content, className)}>
42+
<Dia.Title className={styles.title} hidden={hideTitle}>
43+
{title}
44+
</Dia.Title>
2845
{description && (
2946
<Dia.Description hidden={hideDescription} className={styles.description}>
3047
{description}
@@ -39,3 +56,22 @@ export const Dialog = ({
3956
};
4057

4158
Dialog.Close = Dia.Close;
59+
60+
const Hidden = ({ children }: { children: React.ReactNode }) => (
61+
<span
62+
style={{
63+
position: "absolute",
64+
border: 0,
65+
width: 1,
66+
height: 1,
67+
padding: 0,
68+
margin: -1,
69+
overflow: "hidden",
70+
clip: "rect(0, 0, 0, 0)",
71+
whiteSpace: "nowrap",
72+
wordWrap: "normal",
73+
}}
74+
>
75+
{children}
76+
</span>
77+
);

web/src/components/dimensions/dimensions.module.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,7 @@
120120
margin-bottom: 1rem;
121121
opacity: 0.6;
122122
}
123+
124+
.detailsModal.detailsModal {
125+
max-width: 35em;
126+
}

web/src/components/dimensions/index.tsx

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
useDimension,
1515
} from "../../api";
1616

17+
import { cls } from "../../utils";
18+
import { Dialog } from "../dialog";
1719
import { BrowserIcon, MobileDeviceIcon, OSIcon, ReferrerIcon } from "../icons";
1820
import { countryCodeToFlag, formatFullUrl, formatHost, getHref, tryParseUrl } from "./utils";
1921

@@ -113,18 +115,72 @@ export const DimensionTable = ({
113115
</div>
114116
)}
115117
</div>
116-
<button
117-
type="button"
118-
className={`${styles.showMore} ${(dataTruncated?.length ?? 0) === 0 ? styles.showMoreHidden : ""}`}
119-
onClick={() => console.log("show more")}
120-
>
121-
<ZoomIn size={16} />
122-
Show details
123-
</button>
118+
<DetailsModal project={project} dimension={dimension} metric={metric} range={range} />
124119
</>
125120
);
126121
};
127122

123+
const DetailsModal = ({
124+
project,
125+
dimension,
126+
metric,
127+
range,
128+
}: { project: ProjectResponse; dimension: Dimension; metric: Metric; range: DateRange }) => {
129+
const { data, biggest, order, isLoading } = useDimension({ project, dimension, metric, range });
130+
131+
return (
132+
<Dialog
133+
title={`${dimensionNames[dimension]} - ${metricNames[metric]}`}
134+
description={`Detailed breakdown of ${dimensionNames[dimension]} by ${metricNames[metric]}`}
135+
hideTitle
136+
hideDescription
137+
showClose
138+
className={styles.detailsModal}
139+
trigger={() => (
140+
<button
141+
type="button"
142+
className={cls(styles.showMore, (data?.length ?? 0) === 0 && styles.showMoreHidden)}
143+
onClick={() => console.log("show more")}
144+
>
145+
<ZoomIn size={16} />
146+
Show details
147+
</button>
148+
)}
149+
>
150+
<div className={styles.dimensionTable} style={{ "--count": data?.length } as React.CSSProperties}>
151+
<div className={styles.dimensionHeader}>
152+
<div>{dimensionNames[dimension]}</div>
153+
<div>{metricNames[metric]}</div>
154+
</div>
155+
{data?.map((d) => {
156+
return (
157+
<div
158+
key={d.dimensionValue}
159+
style={{ order: order?.indexOf(d.dimensionValue) }}
160+
className={styles.dimensionRow}
161+
>
162+
<DimensionValueBar value={d.value} biggest={biggest}>
163+
<DimensionLabel dimension={dimension} value={d} />
164+
</DimensionValueBar>
165+
<div>{formatMetricVal(metric, d.value)}</div>
166+
</div>
167+
);
168+
})}
169+
{isLoading && data?.length === 0 && (
170+
<div className={styles.dimensionEmpty}>
171+
<div>Loading...</div>
172+
</div>
173+
)}
174+
{!isLoading && data?.length === 0 && (
175+
<div className={styles.dimensionEmpty}>
176+
<div>No data available</div>
177+
</div>
178+
)}
179+
</div>
180+
</Dialog>
181+
);
182+
};
183+
128184
const dimensionLabels: Record<Dimension, (value: DimensionTableRow) => React.ReactNode> = {
129185
platform: (value) => (
130186
<>

web/src/components/project.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { api, useDimension, useQuery } from "../api";
99
import type { DateRange, Metric, ProjectResponse, StatsResponse } from "../api";
1010
import { type RangeName, resolveRange } from "../api/ranges";
1111

12+
import { cls } from "../utils";
1213
import { DimensionCard, DimensionTabs, DimensionTabsCard, cardStyles } from "./dimensions";
1314
import { LiveVisitorCount, ProjectOverview, SelectRange } from "./projects";
1415

@@ -96,7 +97,7 @@ const GeoCard = ({ project, metric, range }: { project: ProjectResponse; metric:
9697
});
9798

9899
return (
99-
<div className={`${cardStyles} ${styles.geoCard}`} data-full-width="true">
100+
<div className={cls(cardStyles, styles.geoCard)} data-full-width="true">
100101
<div className={styles.geoMap}>
101102
<Suspense fallback={null}>
102103
<WorldMap data={data ?? []} metric={metric} />

web/src/components/projects.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import CountUp from "react-countup";
88
import { type Metric, type ProjectResponse, type StatsResponse, api, metricNames, useMe, useQuery } from "../api";
99
import { type RangeName, rangeNames, resolveRange } from "../api/ranges";
1010
import { getUsername } from "../api/utils";
11+
import { cls } from "../utils";
1112
import { LineGraph, toDataPoints } from "./graph";
1213

1314
const signedIn = getUsername();
@@ -106,7 +107,7 @@ export const SelectRange = ({ onSelect, range }: { onSelect: (name: RangeName) =
106107
};
107108

108109
return (
109-
<details ref={detailsRef} className={`dropdown ${styles.selectRange}`}>
110+
<details ref={detailsRef} className={cls("dropdown", styles.selectRange)}>
110111
<summary>{rangeNames[range]}</summary>
111112
<ul>
112113
{Object.entries(rangeNames).map(([key, value]) => (
@@ -221,7 +222,7 @@ export const ProjectOverview = ({
221222
{detailsElement?.()}
222223
</div>
223224

224-
<div className={`${graphClassName} ${styles.graph}`}>
225+
<div className={cls(graphClassName, styles.graph)}>
225226
<LineGraph title={metricNames[metric]} data={chartData || []} range={graphRange} />
226227
</div>
227228
</div>

web/src/components/settings/tables.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@ import {
2424
useProjects,
2525
useUsers,
2626
} from "../../api";
27+
import { cls } from "../../utils";
2728
import { createToast } from "../toast";
2829

2930
type DropdownOptions = Record<string, ((close: () => void) => JSX.Element) | null>;
3031

3132
const Dropdown = ({ options }: { options: DropdownOptions }) => {
3233
const detailsRef = useRef<HTMLDetailsElement>(null);
3334
return (
34-
<details className={`dropdown ${styles.edit}`} ref={detailsRef}>
35+
<details className={cls("dropdown", styles.edit)} ref={detailsRef}>
3536
<summary>
3637
<EllipsisVerticalIcon />
3738
</summary>

web/src/components/userInfo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import styles from "./userInfo.module.css";
22

33
import { LogOutIcon, SettingsIcon, UserIcon } from "lucide-react";
44
import { api, getUsername } from "../api";
5+
import { cls } from "../utils";
56

67
export const LoginButton = () => {
78
const username = getUsername();
@@ -18,7 +19,7 @@ export const LoginButton = () => {
1819
);
1920

2021
return (
21-
<details className={`dropdown ${styles.user}`}>
22+
<details className={cls("dropdown", styles.user)}>
2223
<summary role="button" className="outline secondary">
2324
<UserIcon size="24" />
2425
{username}

web/src/components/worldmap.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export const WorldMap = ({
7676
</Geographies>
7777
</ZoomableGroup>
7878
</ComposableMap>
79-
<Tooltip id="map" className={`${styles.tooltipContainer}`} classNameArrow={styles.reset} disableStyleInjection>
79+
<Tooltip id="map" className={styles.tooltipContainer} classNameArrow={styles.reset} disableStyleInjection>
8080
{currentGeo && (
8181
<div className={styles.tooltip} data-theme="dark">
8282
<h2>{metricNames[metric]}</h2>

0 commit comments

Comments
 (0)