From 5927f6cf618759a582190f47d7ac960fcd1495da Mon Sep 17 00:00:00 2001 From: Artem Bulgakov Date: Thu, 19 Dec 2024 20:53:59 +0300 Subject: [PATCH] feat(maps): add button to copy url to the area --- src/app/routes/_with_menu/maps.tsx | 15 +++-- src/components/maps/MapsPage.tsx | 19 ++++++- src/components/maps/MapsPageTabs.tsx | 6 +- src/components/maps/viewer/DetailsPopup.tsx | 63 ++++++++++++++++++++- src/components/maps/viewer/MapView.tsx | 2 +- src/components/maps/viewer/MapViewer.tsx | 11 ++-- src/lib/maps/links.ts | 15 +++++ 7 files changed, 110 insertions(+), 21 deletions(-) create mode 100644 src/lib/maps/links.ts diff --git a/src/app/routes/_with_menu/maps.tsx b/src/app/routes/_with_menu/maps.tsx index 23e5860..333d120 100644 --- a/src/app/routes/_with_menu/maps.tsx +++ b/src/app/routes/_with_menu/maps.tsx @@ -7,15 +7,20 @@ import { Helmet } from "react-helmet-async"; export const Route = createFileRoute("/_with_menu/maps")({ validateSearch: ( search: Record, - ): { sceneId?: string; q?: string } => { + ): { scene?: string; q?: string; area?: string } => { return { - sceneId: (search.sceneId as string) ?? undefined, - q: (search.q as string) ?? undefined, + scene: search.scene + ? search.scene.toString() + : search.sceneId // Backward compatibility with 'sceneId' query parameter + ? search.sceneId.toString() + : undefined, + area: search.area ? search.area.toString() : undefined, + q: search.q ? search.q.toString() : undefined, }; }, component: function RouteComponent() { - const { sceneId, q } = Route.useSearch(); + const { scene, area, q } = Route.useSearch(); return ( <> @@ -28,7 +33,7 @@ export const Route = createFileRoute("/_with_menu/maps")({ - + ); }, diff --git a/src/components/maps/MapsPage.tsx b/src/components/maps/MapsPage.tsx index 6d2b25f..4a212ac 100644 --- a/src/components/maps/MapsPage.tsx +++ b/src/components/maps/MapsPage.tsx @@ -5,9 +5,11 @@ import { useEffect } from "react"; export function MapsPage({ sceneId, + areaId, q, }: { sceneId: string | undefined; + areaId: string | undefined; q: string | undefined; }) { const navigate = useNavigate(); @@ -23,13 +25,19 @@ export function MapsPage({ }, ); + const requestedArea = scenes + ?.flatMap((scene) => scene.areas) + .find((area) => area.svg_polygon_id === areaId); + const requestedAreas = requestedArea ? [requestedArea] : undefined; + useEffect(() => { + if (!searchResult || searchResult.length === 0) return; // Show correct floor by navigating to URL with first sceneId - const firstSceneId = searchResult?.[0]?.scene_id; + const firstSceneId = searchResult[0].scene_id; if (firstSceneId !== undefined && firstSceneId !== sceneId) { navigate({ to: "/maps", - search: { sceneId: firstSceneId, q: q }, + search: { scene: firstSceneId, q: q }, replace: true, // Do not add useless history entries not to break the back button }); } @@ -46,7 +54,12 @@ export function MapsPage({ return (
- + res.area) ?? requestedAreas ?? [] + } + />
diff --git a/src/components/maps/MapsPageTabs.tsx b/src/components/maps/MapsPageTabs.tsx index f1637a3..8dbe305 100644 --- a/src/components/maps/MapsPageTabs.tsx +++ b/src/components/maps/MapsPageTabs.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from "react"; export function MapsPageTabs() { const navigate = useNavigate(); const { data: scenes } = $maps.useQuery("get", "/scenes/"); - const { sceneId, q } = useLocation({ select: ({ search }) => search }); + const { scene: sceneId, q } = useLocation({ select: ({ search }) => search }); const [searchText, setSearchText] = useState(q ?? ""); const inputRef = useRef(null); @@ -20,7 +20,7 @@ export function MapsPageTabs() { inputRef.current?.blur(); // Set search query in URL if (searchText) { - navigate({ to: "/maps", search: { q: searchText, sceneId } }); + navigate({ to: "/maps", search: { q: searchText, scene: sceneId } }); } }; @@ -50,7 +50,7 @@ export function MapsPageTabs() { void; @@ -84,10 +90,12 @@ export function DetailsPopup({ {...getFloatingProps()} className="z-10 flex max-w-md flex-col gap-2 rounded-2xl bg-primary p-4 text-sm text-contrast drop-shadow-md" > -
+
- Room: {area.title} + {area.title}
+ +
{area.description && (
@@ -133,3 +141,52 @@ export function DetailsPopup({ ); } + +function ShareButton({ + scene, + area, +}: { + scene: mapsTypes.SchemaScene; + area: mapsTypes.SchemaArea; +}) { + const [_, _copy] = useCopyToClipboard(); + const [copied, setCopied] = useState(false); + const [timer, setTimer] = useState(); + + const copy = () => { + const url = getMapAreaUrl(scene, area); + + _copy(url).then((ok) => { + if (timer !== undefined) { + clearTimeout(timer); + } + if (ok) { + setCopied(true); + setTimer(setTimeout(() => setCopied(false), 1500)); + } else { + setCopied(false); + } + }); + }; + + return ( + + {!copied ? "Share link to this room" : "Link copied!"} +
+ } + > + + + ); +} diff --git a/src/components/maps/viewer/MapView.tsx b/src/components/maps/viewer/MapView.tsx index 8532379..721e927 100644 --- a/src/components/maps/viewer/MapView.tsx +++ b/src/components/maps/viewer/MapView.tsx @@ -8,7 +8,7 @@ export function MapView({ highlightAreas, }: { scene: mapsTypes.SchemaScene; - highlightAreas: mapsTypes.SchemaSearchResult[]; + highlightAreas: mapsTypes.SchemaArea[]; }) { const [fullscreen, setFullscreen] = useState(false); const switchFullscreen = useCallback(() => setFullscreen((v) => !v), []); diff --git a/src/components/maps/viewer/MapViewer.tsx b/src/components/maps/viewer/MapViewer.tsx index d716c05..6ec10ec 100644 --- a/src/components/maps/viewer/MapViewer.tsx +++ b/src/components/maps/viewer/MapViewer.tsx @@ -9,7 +9,7 @@ export const MapViewer = memo(function MapViewer({ highlightAreas, }: { scene: mapsTypes.SchemaScene; - highlightAreas: mapsTypes.SchemaSearchResult[]; + highlightAreas: mapsTypes.SchemaArea[]; }) { const containerRef = useRef(null); const imageRef = useRef(null); @@ -277,9 +277,7 @@ export const MapViewer = memo(function MapViewer({ const rect = containerRef.current.getBoundingClientRect(); const imageRect = imageRef.current.getBoundingClientRect(); - const areaIds = highlightAreas.map( - (s) => s.area.svg_polygon_id ?? undefined, - ); + const areaIds = highlightAreas.map((s) => s.svg_polygon_id ?? undefined); const areas = areaIds.map((id) => imageRef.current?.querySelector(`[id="${id}"]`), ); @@ -322,7 +320,7 @@ export const MapViewer = memo(function MapViewer({ // Show popup if (highlightAreas.length === 1) { - const area = highlightAreas[0].area; + const area = highlightAreas[0]; const el = imageRef.current?.querySelector( `[id="${area.svg_polygon_id}"]`, ); @@ -348,7 +346,7 @@ export const MapViewer = memo(function MapViewer({ 0%, 100% { opacity: 0.2; } 50% { opacity: 0.5; } } - ${highlightAreas.map((s) => `[id="${s.area.svg_polygon_id}"]`).join(",")} { + ${highlightAreas.map((s) => `[id="${s.svg_polygon_id}"]`).join(",")} { fill: violet !important; animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite; } @@ -364,6 +362,7 @@ export const MapViewer = memo(function MapViewer({ )}