Skip to content

Commit 5927f6c

Browse files
feat(maps): add button to copy url to the area
1 parent 1e0cb58 commit 5927f6c

File tree

7 files changed

+110
-21
lines changed

7 files changed

+110
-21
lines changed

src/app/routes/_with_menu/maps.tsx

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@ import { Helmet } from "react-helmet-async";
77
export const Route = createFileRoute("/_with_menu/maps")({
88
validateSearch: (
99
search: Record<string, unknown>,
10-
): { sceneId?: string; q?: string } => {
10+
): { scene?: string; q?: string; area?: string } => {
1111
return {
12-
sceneId: (search.sceneId as string) ?? undefined,
13-
q: (search.q as string) ?? undefined,
12+
scene: search.scene
13+
? search.scene.toString()
14+
: search.sceneId // Backward compatibility with 'sceneId' query parameter
15+
? search.sceneId.toString()
16+
: undefined,
17+
area: search.area ? search.area.toString() : undefined,
18+
q: search.q ? search.q.toString() : undefined,
1419
};
1520
},
1621

1722
component: function RouteComponent() {
18-
const { sceneId, q } = Route.useSearch();
23+
const { scene, area, q } = Route.useSearch();
1924
return (
2025
<>
2126
<Helmet>
@@ -28,7 +33,7 @@ export const Route = createFileRoute("/_with_menu/maps")({
2833

2934
<Topbar title="Maps" />
3035
<MapsPageTabs />
31-
<MapsPage sceneId={sceneId} q={q} />
36+
<MapsPage sceneId={scene} areaId={area} q={q} />
3237
</>
3338
);
3439
},

src/components/maps/MapsPage.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import { useEffect } from "react";
55

66
export function MapsPage({
77
sceneId,
8+
areaId,
89
q,
910
}: {
1011
sceneId: string | undefined;
12+
areaId: string | undefined;
1113
q: string | undefined;
1214
}) {
1315
const navigate = useNavigate();
@@ -23,13 +25,19 @@ export function MapsPage({
2325
},
2426
);
2527

28+
const requestedArea = scenes
29+
?.flatMap((scene) => scene.areas)
30+
.find((area) => area.svg_polygon_id === areaId);
31+
const requestedAreas = requestedArea ? [requestedArea] : undefined;
32+
2633
useEffect(() => {
34+
if (!searchResult || searchResult.length === 0) return;
2735
// Show correct floor by navigating to URL with first sceneId
28-
const firstSceneId = searchResult?.[0]?.scene_id;
36+
const firstSceneId = searchResult[0].scene_id;
2937
if (firstSceneId !== undefined && firstSceneId !== sceneId) {
3038
navigate({
3139
to: "/maps",
32-
search: { sceneId: firstSceneId, q: q },
40+
search: { scene: firstSceneId, q: q },
3341
replace: true, // Do not add useless history entries not to break the back button
3442
});
3543
}
@@ -46,7 +54,12 @@ export function MapsPage({
4654
return (
4755
<div className="mt-1 flex grow flex-col gap-4 @3xl/content:flex-row">
4856
<div className="min-h-[600px] grow">
49-
<MapView scene={currentScene} highlightAreas={searchResult ?? []} />
57+
<MapView
58+
scene={currentScene}
59+
highlightAreas={
60+
searchResult?.map((res) => res.area) ?? requestedAreas ?? []
61+
}
62+
/>
5063
</div>
5164

5265
<div className="flex w-full shrink-0 flex-col gap-2 px-2 @3xl/content:w-64">

src/components/maps/MapsPageTabs.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react";
66
export function MapsPageTabs() {
77
const navigate = useNavigate();
88
const { data: scenes } = $maps.useQuery("get", "/scenes/");
9-
const { sceneId, q } = useLocation({ select: ({ search }) => search });
9+
const { scene: sceneId, q } = useLocation({ select: ({ search }) => search });
1010
const [searchText, setSearchText] = useState(q ?? "");
1111
const inputRef = useRef<HTMLInputElement>(null);
1212

@@ -20,7 +20,7 @@ export function MapsPageTabs() {
2020
inputRef.current?.blur();
2121
// Set search query in URL
2222
if (searchText) {
23-
navigate({ to: "/maps", search: { q: searchText, sceneId } });
23+
navigate({ to: "/maps", search: { q: searchText, scene: sceneId } });
2424
}
2525
};
2626

@@ -50,7 +50,7 @@ export function MapsPageTabs() {
5050
<Link
5151
key={scene.scene_id}
5252
to="/maps"
53-
search={{ sceneId: scene.scene_id }}
53+
search={{ scene: scene.scene_id }}
5454
className={clsx(
5555
"px-2 py-1",
5656
scene.scene_id === sceneId || (sceneId === undefined && i === 0)

src/components/maps/viewer/DetailsPopup.tsx

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { mapsTypes } from "@/api/maps";
2+
import Tooltip from "@/components/common/Tooltip.tsx";
3+
import { getMapAreaUrl } from "@/lib/maps/links.ts";
24
import {
35
arrow,
46
autoUpdate,
@@ -15,15 +17,19 @@ import {
1517
useTransitionStyles,
1618
} from "@floating-ui/react";
1719
import { Link } from "@tanstack/react-router";
18-
import React, { useEffect, useRef } from "react";
20+
import clsx from "clsx";
21+
import { useEffect, useRef, useState } from "react";
22+
import { useCopyToClipboard } from "usehooks-ts";
1923

2024
export function DetailsPopup({
2125
elementRef,
26+
scene,
2227
area,
2328
isOpen,
2429
setIsOpen,
2530
}: {
2631
elementRef: Element | null;
32+
scene: mapsTypes.SchemaScene;
2733
area: mapsTypes.SchemaArea | undefined;
2834
isOpen: boolean;
2935
setIsOpen: (open: boolean) => void;
@@ -84,10 +90,12 @@ export function DetailsPopup({
8490
{...getFloatingProps()}
8591
className="z-10 flex max-w-md flex-col gap-2 rounded-2xl bg-primary p-4 text-sm text-contrast drop-shadow-md"
8692
>
87-
<div className="flex flex-row gap-2">
93+
<div className="flex flex-row justify-between gap-2">
8894
<div className="text-bold flex whitespace-pre-wrap text-xl [overflow-wrap:anywhere]">
89-
Room: <span className="font-medium">{area.title}</span>
95+
<span className="font-medium">{area.title}</span>
9096
</div>
97+
98+
<ShareButton scene={scene} area={area} />
9199
</div>
92100
{area.description && (
93101
<div className="flex flex-row gap-2">
@@ -133,3 +141,52 @@ export function DetailsPopup({
133141
</FloatingPortal>
134142
);
135143
}
144+
145+
function ShareButton({
146+
scene,
147+
area,
148+
}: {
149+
scene: mapsTypes.SchemaScene;
150+
area: mapsTypes.SchemaArea;
151+
}) {
152+
const [_, _copy] = useCopyToClipboard();
153+
const [copied, setCopied] = useState(false);
154+
const [timer, setTimer] = useState<any>();
155+
156+
const copy = () => {
157+
const url = getMapAreaUrl(scene, area);
158+
159+
_copy(url).then((ok) => {
160+
if (timer !== undefined) {
161+
clearTimeout(timer);
162+
}
163+
if (ok) {
164+
setCopied(true);
165+
setTimer(setTimeout(() => setCopied(false), 1500));
166+
} else {
167+
setCopied(false);
168+
}
169+
});
170+
};
171+
172+
return (
173+
<Tooltip
174+
content={
175+
<div className={copied ? "text-green-700 dark:text-green-500" : ""}>
176+
{!copied ? "Share link to this room" : "Link copied!"}
177+
</div>
178+
}
179+
>
180+
<button
181+
type="button"
182+
className={clsx(
183+
"flex items-center justify-center rounded-full hover:bg-secondary-hover",
184+
copied && "text-green-700 dark:text-green-500",
185+
)}
186+
onClick={() => copy()}
187+
>
188+
<span className="icon-[material-symbols--share-outline] text-2xl" />
189+
</button>
190+
</Tooltip>
191+
);
192+
}

src/components/maps/viewer/MapView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function MapView({
88
highlightAreas,
99
}: {
1010
scene: mapsTypes.SchemaScene;
11-
highlightAreas: mapsTypes.SchemaSearchResult[];
11+
highlightAreas: mapsTypes.SchemaArea[];
1212
}) {
1313
const [fullscreen, setFullscreen] = useState(false);
1414
const switchFullscreen = useCallback(() => setFullscreen((v) => !v), []);

src/components/maps/viewer/MapViewer.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const MapViewer = memo(function MapViewer({
99
highlightAreas,
1010
}: {
1111
scene: mapsTypes.SchemaScene;
12-
highlightAreas: mapsTypes.SchemaSearchResult[];
12+
highlightAreas: mapsTypes.SchemaArea[];
1313
}) {
1414
const containerRef = useRef<HTMLDivElement>(null);
1515
const imageRef = useRef<HTMLDivElement>(null);
@@ -277,9 +277,7 @@ export const MapViewer = memo(function MapViewer({
277277
const rect = containerRef.current.getBoundingClientRect();
278278
const imageRect = imageRef.current.getBoundingClientRect();
279279

280-
const areaIds = highlightAreas.map(
281-
(s) => s.area.svg_polygon_id ?? undefined,
282-
);
280+
const areaIds = highlightAreas.map((s) => s.svg_polygon_id ?? undefined);
283281
const areas = areaIds.map((id) =>
284282
imageRef.current?.querySelector(`[id="${id}"]`),
285283
);
@@ -322,7 +320,7 @@ export const MapViewer = memo(function MapViewer({
322320

323321
// Show popup
324322
if (highlightAreas.length === 1) {
325-
const area = highlightAreas[0].area;
323+
const area = highlightAreas[0];
326324
const el = imageRef.current?.querySelector(
327325
`[id="${area.svg_polygon_id}"]`,
328326
);
@@ -348,7 +346,7 @@ export const MapViewer = memo(function MapViewer({
348346
0%, 100% { opacity: 0.2; }
349347
50% { opacity: 0.5; }
350348
}
351-
${highlightAreas.map((s) => `[id="${s.area.svg_polygon_id}"]`).join(",")} {
349+
${highlightAreas.map((s) => `[id="${s.svg_polygon_id}"]`).join(",")} {
352350
fill: violet !important;
353351
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
354352
}
@@ -364,6 +362,7 @@ export const MapViewer = memo(function MapViewer({
364362
)}
365363
<DetailsPopup
366364
elementRef={popupElement}
365+
scene={scene}
367366
area={popupArea}
368367
isOpen={popupIsOpen}
369368
setIsOpen={setPopupIsOpen}

src/lib/maps/links.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { mapsTypes } from "@/api/maps";
2+
3+
export function getMapAreaUrl(
4+
scene: mapsTypes.SchemaScene,
5+
area: mapsTypes.SchemaArea,
6+
) {
7+
const sceneUrl = `${window.location.origin}/maps?scene=${encodeURIComponent(scene.scene_id)}`;
8+
if (area.svg_polygon_id) {
9+
return `${sceneUrl}&area=${encodeURIComponent(area.svg_polygon_id)}`;
10+
} else if (area.title) {
11+
return `${sceneUrl}&q=${encodeURIComponent(area.title)}`;
12+
} else {
13+
return sceneUrl;
14+
}
15+
}

0 commit comments

Comments
 (0)