Skip to content

Commit 0c52a28

Browse files
chore: improve worldmap component
Signed-off-by: Henry Gressmann <[email protected]>
1 parent 07d9bb3 commit 0c52a28

File tree

6 files changed

+147
-76
lines changed

6 files changed

+147
-76
lines changed

web/bun.lockb

2.47 KB
Binary file not shown.

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"@nivo/line": "^0.87.0",
1717
"@picocss/pico": "^2.0.6",
1818
"@radix-ui/react-dialog": "^1.1.1",
19+
"@radix-ui/react-tabs": "^1.1.0",
1920
"@react-spring/web": "^9.7.4",
2021
"@scaleway/use-query-params": "^5.0.5",
2122
"@tanstack/react-query": "^5.51.23",

web/src/components/project.module.css

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@
99
}
1010
}
1111

12+
.tabs {
13+
.tabsList {
14+
display: flex;
15+
gap: 1rem;
16+
margin-bottom: 1rem;
17+
}
18+
19+
button {
20+
all: unset;
21+
cursor: pointer;
22+
23+
&[aria-selected="true"] {
24+
text-decoration: underline;
25+
font-weight: 600;
26+
}
27+
28+
&:last-of-type {
29+
margin-right: auto;
30+
}
31+
}
32+
}
33+
1234
.project {
1335
animation: loadIn 0.2s linear;
1436
display: flex;
@@ -46,10 +68,6 @@ div.graph {
4668
gap: 1rem;
4769
row-gap: 1rem;
4870
margin: 0 -0.4rem;
49-
50-
@media (max-width: 768px) {
51-
grid-template-columns: 1fr;
52-
}
5371
}
5472

5573
.card {
@@ -65,12 +83,42 @@ div.graph {
6583
}
6684

6785
.geoCard {
68-
display: flex;
69-
gap: 2rem;
86+
padding: 0;
7087
> div {
88+
display: flex;
89+
90+
> div {
91+
flex: 1;
92+
flex-direction: column;
93+
gap: 0.5rem;
94+
}
95+
}
96+
97+
.geoMap {
98+
display: flex;
7199
flex: 1;
72-
flex-direction: column;
73-
gap: 0.5rem;
100+
position: relative;
101+
102+
&::after {
103+
content: "";
104+
position: absolute;
105+
background: linear-gradient(
106+
to right,
107+
transparent,
108+
var(--pico-card-background-color)
109+
);
110+
right: 0;
111+
top: 0;
112+
bottom: 0;
113+
width: 2rem;
114+
}
115+
}
116+
117+
.geoTable {
118+
> div {
119+
padding: 1rem;
120+
padding-left: 0;
121+
}
74122
}
75123
}
76124

web/src/components/project.tsx

Lines changed: 73 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
21
import styles from "./project.module.css";
32

3+
import { lazy, Suspense, useEffect, useMemo, useState } from "react";
4+
import { LinkIcon, LockIcon } from "lucide-react";
5+
import { useLocalStorage } from "@uidotdev/usehooks";
6+
import * as Tabs from "@radix-ui/react-tabs";
7+
8+
import { resolveRange, type RangeName } from "../api/ranges";
49
import { api, dimensionNames, formatMetricVal, metricNames, useQuery } from "../api";
510
import type { DateRange, Dimension, DimensionTableRow, Metric, ProjectResponse, StatsResponse } from "../api";
611

7-
import { useLocalStorage } from "@uidotdev/usehooks";
8-
import { LiveVisitorCount, ProjectOverview, SelectRange } from "./projects";
9-
import { resolveRange, type RangeName } from "../api/ranges";
1012
import { BrowserIcon, MobileDeviceIcon, OSIcon, ReferrerIcon } from "./icons";
11-
import { LinkIcon, LockIcon } from "lucide-react";
12-
const server = typeof window === "undefined";
13+
import { LiveVisitorCount, ProjectOverview, SelectRange } from "./projects";
1314

1415
const WorldMap = lazy(() => import("./worldmap").then((module) => ({ default: module.WorldMap })));
1516

@@ -19,7 +20,7 @@ export const Project = () => {
1920
const [metric, setMetric] = useLocalStorage<Metric>("metric", "views");
2021

2122
useEffect(() => {
22-
if (server) return;
23+
if (typeof window === "undefined") return;
2324
setProjectId(window?.document.location.pathname.split("/").pop() ?? null);
2425
}, []);
2526

@@ -43,33 +44,24 @@ export const Project = () => {
4344
renderHeader={(props) => <ProjectHeader {...props} range={dateRange} setRange={setDateRange} />}
4445
/>
4546
<div className={styles.tables}>
46-
<Card>
47-
<DimTable project={data} dimension={"platform"} metric={metric} range={resolveRange(dateRange).range} />
48-
</Card>
49-
50-
<Card>
51-
<DimTable project={data} dimension={"browser"} metric={metric} range={resolveRange(dateRange).range} />
52-
</Card>
53-
<Card fullWidth>
54-
<GeoCard project={data} metric={metric} range={resolveRange(dateRange).range} />
55-
</Card>
5647
<Card>
5748
<DimTable project={data} dimension={"url"} metric={metric} range={resolveRange(dateRange).range} />
5849
</Card>
5950
<Card>
60-
<DimTable project={data} dimension={"fqdn"} metric={metric} range={resolveRange(dateRange).range} />
51+
<DimTable project={data} dimension={"referrer"} metric={metric} range={resolveRange(dateRange).range} />
6152
</Card>
53+
<GeoCard project={data} metric={metric} range={resolveRange(dateRange).range} />
6254
<Card>
63-
<DimTable project={data} dimension={"mobile"} metric={metric} range={resolveRange(dateRange).range} />
55+
<DimTable project={data} dimension={"platform"} metric={metric} range={resolveRange(dateRange).range} />
6456
</Card>
6557
<Card>
66-
<DimTable project={data} dimension={"referrer"} metric={metric} range={resolveRange(dateRange).range} />
58+
<DimTable project={data} dimension={"browser"} metric={metric} range={resolveRange(dateRange).range} />
6759
</Card>
6860
<Card>
69-
<DimTable project={data} dimension={"city"} metric={metric} range={resolveRange(dateRange).range} />
61+
<DimTable project={data} dimension={"fqdn"} metric={metric} range={resolveRange(dateRange).range} />
7062
</Card>
7163
<Card>
72-
<DimTable project={data} dimension={"country"} metric={metric} range={resolveRange(dateRange).range} />
64+
<DimTable project={data} dimension={"mobile"} metric={metric} range={resolveRange(dateRange).range} />
7365
</Card>
7466
</div>
7567
</div>
@@ -108,18 +100,6 @@ const ProjectHeader = ({
108100
);
109101
};
110102

111-
const Entities = ({ entities }: { entities: { id: string; displayName: string }[] }) => {
112-
return (
113-
<div className={styles.entities}>
114-
{entities.map((entity) => (
115-
<div key={entity.id} className={styles.entity}>
116-
<h3>{entity.displayName}</h3>
117-
</div>
118-
))}
119-
</div>
120-
);
121-
};
122-
123103
const Card = ({ children, fullWidth }: { children: React.ReactNode; fullWidth?: boolean }) => {
124104
return (
125105
<div className={styles.card} data-full-width={fullWidth ?? undefined}>
@@ -149,26 +129,35 @@ const GeoCard = ({ project, metric, range }: { project: ProjectResponse; metric:
149129
const order = useMemo(() => data?.data?.sort((a, b) => b.value - a.value).map((d) => d.dimensionValue), [data]);
150130

151131
return (
152-
<div className={styles.geoCard}>
153-
<div>
154-
<WorldMap data={data?.data} metric={metric} />
155-
</div>
156-
<div>
157-
{data?.data?.map((d) => {
158-
const value = metric === "avg_views_per_session" ? d.value / 1000 : d.value;
159-
const biggestVal = metric === "avg_views_per_session" ? biggest / 1000 : biggest;
160-
161-
return (
162-
<div key={d.dimensionValue} style={{ order: order?.indexOf(d.dimensionValue) }} className={styles.dimRow}>
163-
<DimensionValueBar value={value} biggest={biggestVal}>
164-
<DimensionLabel dimension={"country"} value={d} />
165-
</DimensionValueBar>
166-
167-
<div>{value.toFixed(1).replace(/\.0$/, "") || "0"}</div>
168-
</div>
169-
);
170-
})}
171-
</div>
132+
<div className={`${styles.card} ${styles.geoCard}`} data-full-width="true">
133+
<Suspense>
134+
<div>
135+
<div className={styles.geoMap}>
136+
<WorldMap data={data?.data} metric={metric} />
137+
</div>
138+
<div className={styles.geoTable}>
139+
<Tabs.Root className={styles.tabs} defaultValue="cities">
140+
<Tabs.List className={styles.tabsList}>
141+
<Tabs.Trigger value="countries">Countries</Tabs.Trigger>
142+
<Tabs.Trigger value="cities">Cities</Tabs.Trigger>
143+
<div>{metricNames[metric]}</div>
144+
</Tabs.List>
145+
<Tabs.Content value="countries">
146+
<DimList
147+
value={data?.data ?? []}
148+
dimension={"country"}
149+
metric={metric}
150+
biggest={biggest}
151+
order={order}
152+
/>
153+
</Tabs.Content>
154+
<Tabs.Content value="cities">
155+
<DimTable project={project} dimension={"city"} metric={metric} range={range} noHeader />
156+
</Tabs.Content>
157+
</Tabs.Root>
158+
</div>
159+
</div>
160+
</Suspense>
172161
</div>
173162
);
174163
};
@@ -178,7 +167,8 @@ const DimTable = ({
178167
dimension,
179168
metric,
180169
range,
181-
}: { project: ProjectResponse; dimension: Dimension; metric: Metric; range: DateRange }) => {
170+
noHeader,
171+
}: { project: ProjectResponse; dimension: Dimension; metric: Metric; range: DateRange; noHeader?: boolean }) => {
182172
const { data } = useQuery({
183173
placeholderData: (prev) => prev,
184174
queryKey: ["dimension", project.id, dimension, metric, range],
@@ -200,21 +190,40 @@ const DimTable = ({
200190

201191
return (
202192
<div className={styles.dimTable}>
203-
<div className={styles.header}>
204-
<div>{dimensionNames[dimension]}</div>
205-
<div>{metricNames[metric]}</div>
206-
</div>
207-
{data?.data?.map((d) => {
208-
const value = d.value;
209-
const biggestVal = biggest;
193+
{!noHeader && (
194+
<div className={styles.header}>
195+
<div>{dimensionNames[dimension]}</div>
196+
<div>{metricNames[metric]}</div>
197+
</div>
198+
)}
199+
<DimList value={data?.data ?? []} dimension={dimension} metric={metric} biggest={biggest} order={order} />
200+
</div>
201+
);
202+
};
210203

204+
const DimList = ({
205+
value,
206+
dimension,
207+
metric,
208+
biggest,
209+
order,
210+
}: {
211+
value: DimensionTableRow[];
212+
dimension: Dimension;
213+
metric: Metric;
214+
biggest: number;
215+
order?: string[];
216+
}) => {
217+
return (
218+
<div>
219+
{value.map((d) => {
211220
return (
212221
<div key={d.dimensionValue} style={{ order: order?.indexOf(d.dimensionValue) }} className={styles.dimRow}>
213-
<DimensionValueBar value={value} biggest={biggestVal}>
222+
<DimensionValueBar value={d.value} biggest={biggest}>
214223
<DimensionLabel dimension={dimension} value={d} />
215224
</DimensionValueBar>
216225

217-
<div>{formatMetricVal(metric, value)}</div>
226+
<div>{formatMetricVal(metric, d.value)}</div>
218227
</div>
219228
);
220229
})}

web/src/components/worldmap.module.css

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
.reset {
1+
.tooltipContainer {
22
all: unset;
33
}
44

5+
.worldmap {
6+
display: flex;
7+
flex: 1;
8+
9+
> svg {
10+
flex: 1;
11+
}
12+
13+
:global(.react-tooltip) {
14+
z-index: 10;
15+
}
16+
}
17+
518
.geo {
619
stroke: var(--pico-card-background-color);
720
stroke-width: 1;

web/src/components/worldmap.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ export const WorldMap = ({
3535
projection="geoMercator"
3636
projectionConfig={{
3737
rotate: [0, 0, 0],
38-
center: [0, 30],
39-
scale: 140,
38+
center: [0, 50],
39+
scale: 120,
4040
}}
4141
height={500}
4242
>
@@ -76,7 +76,7 @@ export const WorldMap = ({
7676
</Geographies>
7777
</ZoomableGroup>
7878
</ComposableMap>
79-
<Tooltip id="map" className={styles.reset} 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)