Skip to content

Commit

Permalink
Added hover functionality (#20)
Browse files Browse the repository at this point in the history
* removed defer

* resolved warnings

* modified Popup functionality

* Add route county route. Rework geosearch.

* Resolve conflicts

* Fix my cluster layer error.

* Added hover functionality to list

---------

Co-authored-by: Jay Varner <[email protected]>
  • Loading branch information
CVVSAI and jayvarner authored Dec 3, 2024
1 parent 5354fc4 commit 7f8c383
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 72 deletions.
28 changes: 26 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,13 +1,37 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": ["jsx-a11y"],
"plugins": ["@typescript-eslint", "jsx-a11y", "unused-imports", "react"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"@remix-run/eslint-config",
"@remix-run/eslint-config/node",
"plugin:jsx-a11y/recommended"
],
"rules": {
"@typescript-eslint/ban-ts-comment": "off"
"@typescript-eslint/no-unused-vars": [
"warn",
{
"vars": "all",
"args": "after-used",
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
],
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"args": "after-used",
"argsIgnorePattern": "^_"
}
]
},
"settings": {
"react": {
"version": "detect"
}
}
}
2 changes: 1 addition & 1 deletion app/components/mapping/PointLayer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect } from "react";
import { useContext, useEffect } from "react";
import { MapContext, PlaceContext } from "~/contexts";
import { pulsingDot } from "~/utils/pulsingDot";
import type {
Expand Down
170 changes: 106 additions & 64 deletions app/components/relatedRecords/RelatedPlaces.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { useCallback, useContext, useEffect, useState } from "react";
import { LngLatBounds } from "maplibre-gl";
import { bbox } from "@turf/turf";
import RelatedSection from "./RelatedSection";
import { MapContext, PlaceContext } from "~/contexts";
import { toFeatureCollection } from "~/utils/toFeatureCollection";
import PlaceTooltip from "../mapping/PlaceTooltip";
import PlacePopup from "../mapping/PlacePopup.client";
import { cluster, clusterCount, singlePoint } from "~/mapStyles/geoJSON";
import { Link } from "@remix-run/react";
import type {
GeoJSONSource,
MapLayerMouseEvent,
SourceSpecification,
GeoJSONSource,
} from "maplibre-gl";
import type { ESRelatedPlace } from "~/esTypes";
import { ClientOnly } from "remix-utils/client-only";
Expand All @@ -20,50 +23,80 @@ const RelatedPlaces = () => {
const [activePlace, setActivePlace] = useState<ESRelatedPlace | undefined>(
undefined
);
const [hoveredPlace, setHoveredPlace] = useState<ESRelatedPlace | undefined>(
undefined
);
const [placeBounds, setPlaceBounds] = useState<LngLatBounds | undefined>(
undefined
);

const handleMouseEnter = useCallback(
({ features }: MapLayerMouseEvent) => {
if (!map || !features) return;

const hovered = place.places.find(
(relatedPlace) => relatedPlace.uuid === features[0]?.id
);
setHoveredPlace(hovered);
map.getCanvas().style.cursor = "pointer";
},
[map, place.places]
);

const handleMouseLeave = useCallback(() => {
setHoveredPlace(undefined);

if (map) {
map.getCanvas().style.cursor = "";
}
}, [map]);

const handleClick = useCallback(
async ({ features, lngLat, ...rest }: MapLayerMouseEvent) => {
if (!map) return;
async ({ features, lngLat }: MapLayerMouseEvent) => {
if (!map || !features) return;

if (features && features[0].id) {
const clickedPlace = place.places.find(
(relatedPlaces) => relatedPlaces.uuid === features[0].id
);
setActivePlace(clickedPlace);
}
if (features && features[0].properties.cluster) {
const feature = features[0];
if (!feature) return;

if (feature.properties?.cluster) {
const source: GeoJSONSource | undefined = map.getSource(
features[0].layer.source
feature.layer.source
);
if (!source) return;

const zoom = await source.getClusterExpansionZoom(
features[0].properties.cluster_id
feature.properties.cluster_id
);
map.easeTo({
center: lngLat,
zoom,
});
return;
}

const clickedPlace = place.places.find(
(relatedPlace) => relatedPlace.uuid === feature.id
);
setHoveredPlace(undefined);
setActivePlace(clickedPlace);
},
[map, place]
[map, place.places]
);

const handleMouseEnter = useCallback(() => {
if (!map) return;
map.getCanvas().style.cursor = "pointer";
}, [map]);

const handleMouseLeave = useCallback(() => {
if (!map) return;
map.getCanvas().style.cursor = "";
}, [map]);

useEffect(() => {
if (!map) return;
if (!place.places || place.places.length === 0) return;

const geojson = toFeatureCollection(place.places);

const bounds = new LngLatBounds(
bbox(geojson) as [number, number, number, number]
);
const newBounds = map.getBounds().extend(bounds);

setPlaceBounds(newBounds);
map.fitBounds(newBounds, { maxZoom: 14 });

const placesSource: SourceSpecification = {
type: "geojson",
data: geojson,
Expand All @@ -83,51 +116,37 @@ const RelatedPlaces = () => {
fillColor: clusterFillColor ?? "#1d4ed8",
});

if (!map.getLayer(clusterLayer.id)) {
map.addLayer(clusterLayer);
}

const countLayer = clusterCount({
id: `counts-${place.uuid}`,
source: `${place.uuid}-places`,
textColor: clusterTextColor ?? "white",
});

if (!map.getLayer(countLayer.id)) {
map.addLayer(countLayer);
}

const unclusteredLayer = singlePoint(
`points-${place.uuid}`,
`${place.uuid}-places`
);

if (!map.getLayer(unclusteredLayer.id)) {
map.addLayer(unclusteredLayer);
}
if (!map.getLayer(clusterLayer.id)) map.addLayer(clusterLayer);
if (!map.getLayer(countLayer.id)) map.addLayer(countLayer);
if (!map.getLayer(unclusteredLayer.id)) map.addLayer(unclusteredLayer);

map.on("mouseenter", clusterLayer.id, handleMouseEnter);
map.on("mouseenter", unclusteredLayer.id, handleMouseEnter);
map.on("mouseleave", clusterLayer.id, handleMouseLeave);
map.on("mouseleave", unclusteredLayer.id, handleMouseLeave);
map.on("click", clusterLayer.id, handleClick);
map.on("click", unclusteredLayer.id, handleClick);
map.on("click", clusterLayer.id, handleClick);

return () => {
map.off("mouseenter", clusterLayer.id, handleMouseEnter);
map.off("mouseleave", clusterLayer.id, handleMouseLeave);
map.off("mouseenter", unclusteredLayer.id, handleMouseEnter);
map.off("mouseleave", unclusteredLayer.id, handleMouseLeave);
map.off("click", clusterLayer.id, handleClick);
map.off("click", unclusteredLayer.id, handleClick);
map.off("click", clusterLayer.id, handleClick);
if (map.getLayer(clusterLayer.id)) map.removeLayer(clusterLayer.id);
if (map.getLayer(countLayer.id)) map.removeLayer(countLayer.id);
if (map.getLayer(unclusteredLayer.id))
map.removeLayer(unclusteredLayer.id);
if (map.getSource(`${place.uuid}-places`))
map.removeSource(`${place.uuid}-places`);
if (map.getSource(`${place.uuid}-places`))
map.removeSource(`${place.uuid}-places`);
};
}, [map, place, handleClick, handleMouseEnter, handleMouseLeave]);

Expand All @@ -137,9 +156,17 @@ const RelatedPlaces = () => {
<div className="grid grid-cols-1 md:grid-cols-2">
{place.places.map((relatedPlace) => {
return (
<div key={`related-place-${relatedPlace.uuid}`}>
<div
key={`related-place-${relatedPlace.uuid}`}
onMouseEnter={() => setHoveredPlace(relatedPlace)}
onMouseLeave={() => setHoveredPlace(undefined)}
>
<button
className={`text-black/75 hover:text-black text-left md:py-1 ${activePlace === relatedPlace ? "underline font-bold" : ""}`}
className={`text-black/75 text-left md:py-1 ${
hoveredPlace?.uuid === relatedPlace.uuid
? "bg-gray-200 font-bold"
: ""
} ${activePlace === relatedPlace ? "underline font-bold" : ""}`}
onClick={() => {
setActivePlace(relatedPlace);
}}
Expand All @@ -148,28 +175,43 @@ const RelatedPlaces = () => {
</button>
<ClientOnly>
{() => (
<PlacePopup
location={{
lat: relatedPlace.location.lat,
lon: relatedPlace.location.lon,
}}
show={activePlace?.uuid === relatedPlace.uuid}
onClose={() => setActivePlace(undefined)}
>
<h4 className="text-xl">{relatedPlace.name}</h4>
<div
dangerouslySetInnerHTML={{
__html: relatedPlace.description ?? "",
<>
<PlacePopup
location={{
lat: relatedPlace.location.lat,
lon: relatedPlace.location.lon,
}}
show={activePlace?.uuid === relatedPlace.uuid}
onClose={() => setActivePlace(undefined)}
zoomToFeature = {false}
>
<h4 className="text-xl">{relatedPlace.name}</h4>
<div
dangerouslySetInnerHTML={{
__html: relatedPlace.description ?? "",
}}
/>
<Link
to={`/places/${relatedPlace.slug}`}
state={{ backTo: place.name }}
className="text-blue-600 underline underline-offset-2 hover:text-blue-900"
>
Read More
</Link>
</PlacePopup>
<PlaceTooltip
location={{
lat: relatedPlace.location.lat,
lon: relatedPlace.location.lon,
}}
/>
<Link
to={`/places/${relatedPlace.slug}`}
state={{ backTo: place.name }}
className="text-blue-600 underline underline-offset-2 hover:text-blue-900"
show={hoveredPlace?.uuid === relatedPlace.uuid}
onClose={() => {}}
anchor="left"
zoomToFeature={false}
>
Read More
</Link>
</PlacePopup>
<h4 className="text-white">{relatedPlace.name}</h4>
</PlaceTooltip>
</>
)}
</ClientOnly>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
import type { LabeledIIIFExternalWebResource } from "@samvera/clover-iiif/image";
import type { ImageService } from "@iiif/presentation-3";
import type { SourceSpecification } from "maplibre-gl";
import { ESPlace } from "./esTypes";
import type { ESPlace } from "./esTypes";

export type Geometry =
| Point
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"@remix-run/serve": "*",
"@types/geojson": "^7946.0.14",
"@types/uuid": "^9.0.8",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"isbot": "^5.1.8",
Expand All @@ -41,8 +43,6 @@
"@turf/turf": "^7.0.0-alpha.114",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0",
"algoliasearch": "^4.24.0",
"autoprefixer": "^10.4.19",
"chroma-js": "^3.1.1",
Expand All @@ -52,6 +52,7 @@
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.34.2",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
"nuka-carousel": "^8.0.1",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
Expand All @@ -69,4 +70,4 @@
"engines": {
"node": ">=18.0.0"
}
}
}
1 change: 0 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export default defineConfig({
plugins: [
remix({
future: {

v3_singleFetch: true,
v3_fetcherPersist: true,
v3_lazyRouteDiscovery: true,
Expand Down

0 comments on commit 7f8c383

Please sign in to comment.