diff --git a/public/connected-devices.png b/public/connected-devices.png
new file mode 100644
index 0000000..4d6c622
Binary files /dev/null and b/public/connected-devices.png differ
diff --git a/public/connected-dots.png b/public/connected-dots.png
new file mode 100644
index 0000000..02f5992
Binary files /dev/null and b/public/connected-dots.png differ
diff --git a/public/hotspot-waves.png b/public/hotspot-waves.png
new file mode 100644
index 0000000..dfd7972
Binary files /dev/null and b/public/hotspot-waves.png differ
diff --git a/public/hotspot.png b/public/hotspot.png
new file mode 100644
index 0000000..ac6c6a6
Binary files /dev/null and b/public/hotspot.png differ
diff --git a/public/kuzco-logo.png b/public/kuzco-logo.png
new file mode 100644
index 0000000..a918400
Binary files /dev/null and b/public/kuzco-logo.png differ
diff --git a/public/latitude.png b/public/latitude.png
new file mode 100644
index 0000000..fe801ce
Binary files /dev/null and b/public/latitude.png differ
diff --git a/public/longitude.png b/public/longitude.png
new file mode 100644
index 0000000..729c586
Binary files /dev/null and b/public/longitude.png differ
diff --git a/public/tech-info.png b/public/tech-info.png
new file mode 100644
index 0000000..6070740
Binary files /dev/null and b/public/tech-info.png differ
diff --git a/src/app/layout.module.css b/src/app/layout.module.css
new file mode 100644
index 0000000..4f09f41
--- /dev/null
+++ b/src/app/layout.module.css
@@ -0,0 +1,3 @@
+.overall {
+ text-rendering: geometricPrecision;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 288677b..ecf57d3 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,13 +1,22 @@
import { GAScript } from "@/components/GAScript"
import { GATracker } from "@/components/GATracker"
import { Header } from "@/components/Header"
-import { HotspotsMap } from "@/components/HotspotsMap"
import { Providers } from "@/components/Providers"
import "@/styles/tailwind.css"
import "focus-visible"
+import { DM_Sans } from "next/font/google"
import Head from "next/head"
import { Suspense } from "react"
import "react-tooltip/dist/react-tooltip.css"
+import styles from "./layout.module.css"
+import { HotspotsMap } from "./new/HotspotsMap"
+import { Nav } from "./new/Nav/Nav"
+
+const dm_sans = DM_Sans({
+ subsets: ["latin"],
+ display: "swap",
+ variable: "--font-dm-sans",
+})
export const metadata = {
manifest: "/manifest.json",
@@ -55,7 +64,11 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
-
+
+
{children}
diff --git a/src/app/new/HotspotsMap/ConnectionPower.tsx b/src/app/new/HotspotsMap/ConnectionPower.tsx
new file mode 100644
index 0000000..d96e211
--- /dev/null
+++ b/src/app/new/HotspotsMap/ConnectionPower.tsx
@@ -0,0 +1,29 @@
+import { RssiPill } from "@/components/shared/RssiPill"
+import clsx from "clsx"
+
+export const ConnectionPower = () => {
+ return (
+
+
+
Signal Strength
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/HexHotspotItem.tsx b/src/app/new/HotspotsMap/HexHotspotItem.tsx
new file mode 100644
index 0000000..6d6542a
--- /dev/null
+++ b/src/app/new/HotspotsMap/HexHotspotItem.tsx
@@ -0,0 +1,68 @@
+"use client"
+
+import { usePreferences } from "@/context/usePreferences"
+import animalHash from "angry-purple-tiger"
+import Link from "next/link"
+import { gaEvent } from "../GATracker"
+import { HeliumIotIcon } from "../icons/HeliumIotIcon"
+import { HeliumMobileIcon } from "../icons/HeliumMobileIcon"
+import { Hotspot } from "./HexHotspots"
+
+type HexHotSpotItemProps = {
+ hotspot: Hotspot
+}
+
+export const HexHotSpotItem = ({ hotspot }: HexHotSpotItemProps) => {
+ const { provider } = usePreferences()
+
+ const hotspotName = animalHash(hotspot.hotspot_id)
+ const hasSmallCells = hotspot.cells.length > 0
+ const Avatar = hasSmallCells ? HeliumMobileIcon : HeliumIotIcon
+ const subtitle = hasSmallCells
+ ? `${hotspot.cells.length} small cell${
+ hotspot.cells.length === 1 ? "" : "s"
+ }`
+ : "IoT Hotspot"
+
+ return (
+
+
+
{
+ if (!!provider) {
+ gaEvent({
+ action: "outbound_click",
+ event: {
+ description: provider.label,
+ },
+ })
+ }
+ }}
+ >
+
+
+
+
+
+ {hotspotName}
+
+
+ {subtitle}
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/HexHotspots.tsx b/src/app/new/HotspotsMap/HexHotspots.tsx
new file mode 100644
index 0000000..651c872
--- /dev/null
+++ b/src/app/new/HotspotsMap/HexHotspots.tsx
@@ -0,0 +1,113 @@
+import { Tooltip } from "@/app/stats/components/Tooltip"
+import { PreferencesProvider } from "@/context/usePreferences"
+import clsx from "clsx"
+import { HexHotSpotItem } from "./HexHotspotItem"
+
+interface SmallCell {
+ cell_id: string
+}
+
+export interface Hotspot {
+ hotspot_id: string
+ active: boolean
+ cells: SmallCell[]
+}
+
+interface HexData {
+ hex: string
+ resolution: number
+ hotspots: Hotspot[]
+}
+
+const RECENT = "recently rewarded"
+const NOT_RECENT = "not recently rewarded"
+
+const TOOLTIP_DESCRIPTIONS = {
+ [RECENT]: "A Hotspot that has received rewards in the past 30 days.",
+ [NOT_RECENT]:
+ "A Hotspot that has not received rewards in the past 30 days. Such a hotstop is most likely offline. It is also possible for it to be online but not rewarded if it is not transmitting data, not participating in PoC, or only recently online.",
+}
+
+function getGroupedHotspots(hotspots: Hotspot[]) {
+ const groupedHotspots: {
+ [RECENT]: Hotspot[]
+ [NOT_RECENT]: Hotspot[]
+ } = {
+ [RECENT]: [],
+ [NOT_RECENT]: [],
+ }
+
+ hotspots.forEach((hotspot) => {
+ const group = hotspot.active ? RECENT : NOT_RECENT
+ groupedHotspots[group].push(hotspot)
+ })
+
+ return groupedHotspots
+}
+
+export async function HexHotspots({ hexId }: { hexId: string }) {
+ const { hotspots } = (await fetch(
+ `${process.env.NEXT_PUBLIC_HOTSPOTTY_EXPLORER_API_URL}/hex/${hexId}`,
+ {
+ headers: {
+ Authorization: `bearer ${process.env.NEXT_PUBLIC_HOTSPOTTY_EXPLORER_API_TOKEN}`,
+ },
+ }
+ ).then((res) => res.json())) as HexData
+
+ const groupedList = getGroupedHotspots(hotspots)
+
+ if (hotspots.length === 0) {
+ return (
+
+ This hex contains no Hotspots.
+
+ )
+ }
+
+ return (
+
+ {(Object.keys(groupedList) as Array
).map(
+ (group) => {
+ if (groupedList[group].length === 0) return
+ return (
+
+
+
+ {group}
+
+
+
+ {groupedList[group].length} Hotspots
+
+
+
+
+ {groupedList[group].map((hotspot) => (
+
+ ))}
+
+
+
+ )
+ }
+ )}
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/LoadingHexHotspots.tsx b/src/app/new/HotspotsMap/LoadingHexHotspots.tsx
new file mode 100644
index 0000000..348897f
--- /dev/null
+++ b/src/app/new/HotspotsMap/LoadingHexHotspots.tsx
@@ -0,0 +1,39 @@
+import clsx from "clsx"
+
+export function LoadingHexHotspots({ count }: { count: number }) {
+ return (
+
+
+ Active
+
+
+ {[...Array(count).keys()].map((i) => (
+ -
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/MapZoom.tsx b/src/app/new/HotspotsMap/MapZoom.tsx
new file mode 100644
index 0000000..94fa3d7
--- /dev/null
+++ b/src/app/new/HotspotsMap/MapZoom.tsx
@@ -0,0 +1,64 @@
+import { MinusIcon, PlusIcon } from "@heroicons/react/24/outline"
+import clsx from "clsx"
+import { useEffect, useState } from "react"
+import { useMap } from "react-map-gl"
+import { MAX_MAP_ZOOM, MIN_MAP_ZOOM } from "./utils"
+
+export const MapZoom = () => {
+ const map = useMap()
+ const [_currentZoom, setCurrentZoom] = useState(map.current?.getZoom())
+ const zoom = map.current?.getZoom()
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setCurrentZoom(map.current?.getZoom())
+ }, 250)
+ return () => clearInterval(interval)
+ }, [map, setCurrentZoom])
+
+ const isZoomInDisabled = zoom === MAX_MAP_ZOOM
+ const isZoomOutDisabled = zoom === MIN_MAP_ZOOM
+
+ return (
+
+
+
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/NetworkCoverageLayer.tsx b/src/app/new/HotspotsMap/NetworkCoverageLayer.tsx
new file mode 100644
index 0000000..9560ab9
--- /dev/null
+++ b/src/app/new/HotspotsMap/NetworkCoverageLayer.tsx
@@ -0,0 +1,60 @@
+import { useTheme } from "next-themes"
+import { Fragment } from "react"
+import { Layer, Source } from "react-map-gl"
+import {
+ MIN_HEXES_ZOOM,
+ MIN_HEX_LABELS_ZOOM,
+ NetworkCoverageLayerOption,
+ POINTS_AND_HEXES_OVERLAP,
+ getBlurredPointStyle,
+ getHexFillStyle,
+ getHexLabelStyle,
+ hexLabelLayout,
+} from "./utils"
+
+export function NetworkCoverageLayer({
+ layer: { color, sourceDomain, points, hexes },
+ ...props
+}: {
+ layer: NetworkCoverageLayerOption
+}) {
+ const { resolvedTheme } = useTheme()
+ return (
+
+
+
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/SettingsTrigger.tsx b/src/app/new/HotspotsMap/SettingsTrigger.tsx
new file mode 100644
index 0000000..1bfc92a
--- /dev/null
+++ b/src/app/new/HotspotsMap/SettingsTrigger.tsx
@@ -0,0 +1,43 @@
+import { Overlay } from "@/components/shared/Overlay"
+import { Cog6ToothIcon } from "@heroicons/react/24/outline"
+import clsx from "clsx"
+import { useCallback, useState } from "react"
+import { Settings } from "../Settings/Settings"
+
+type SettingsTriggerProps = {
+ isAbsolute?: boolean
+}
+
+export const SettingsTrigger = ({
+ isAbsolute = false,
+}: SettingsTriggerProps) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const closeSettings = useCallback(() => {
+ setIsOpen(false)
+ }, [])
+
+ return (
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/index.tsx b/src/app/new/HotspotsMap/index.tsx
new file mode 100644
index 0000000..d465820
--- /dev/null
+++ b/src/app/new/HotspotsMap/index.tsx
@@ -0,0 +1,181 @@
+"use client"
+
+import maplibregl from "maplibre-gl"
+import "maplibre-gl/dist/maplibre-gl.css"
+import { Protocol } from "pmtiles"
+
+import { gaEvent } from "@/components/GATracker"
+import { PreferencesProvider } from "@/context/usePreferences"
+import { cellToLatLng, cellsToMultiPolygon, getResolution } from "h3-js"
+import { useTheme } from "next-themes"
+import {
+ usePathname,
+ useRouter,
+ useSelectedLayoutSegment,
+ useSelectedLayoutSegments,
+} from "next/navigation"
+import { useCallback, useEffect, useMemo, useRef, useState } from "react"
+import Map, {
+ Layer,
+ MapLayerMouseEvent,
+ MapProvider,
+ MapRef,
+ MapStyle,
+ Source,
+} from "react-map-gl"
+import { ConnectionPower } from "./ConnectionPower"
+import { MapZoom } from "./MapZoom"
+import { NetworkCoverageLayer } from "./NetworkCoverageLayer"
+import { SettingsTrigger } from "./SettingsTrigger"
+import { mapLayersDark } from "./mapLayersDark"
+import { mapLayersLight } from "./mapLayersLight"
+import {
+ HexFeatureDetails,
+ INITIAL_MAP_VIEW_STATE,
+ MAP_CONTAINER_STYLE,
+ MAX_MAP_ZOOM,
+ MIN_MAP_ZOOM,
+ ZOOM_BY_HEX_RESOLUTION,
+ getHexOutlineStyle,
+ networkLayers,
+} from "./utils"
+
+export function HotspotsMap({ children }: { children: React.ReactNode }) {
+ const { resolvedTheme } = useTheme()
+ const router = useRouter()
+ const pathname = usePathname()
+ const segments = useSelectedLayoutSegments()
+ const segment = useSelectedLayoutSegment()
+ const mapRef = useRef(null)
+ const [selectedHex, setSelectedHex] = useState(null)
+ const [cursor, setCursor] = useState("")
+
+ useEffect(() => {
+ let protocol = new Protocol()
+ maplibregl.addProtocol("pmtiles", protocol.tile)
+ return () => {
+ maplibregl.removeProtocol("pmtiles")
+ }
+ }, [])
+
+ const mapStyle = useMemo(() => {
+ const style: MapStyle = {
+ version: 8,
+ sources: {
+ protomaps: {
+ type: "vector",
+ tiles: [`${process.env.NEXT_PUBLIC_PMTILES_URL}/{z}/{x}/{y}.mvt`],
+ },
+ },
+ glyphs: "https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf",
+ layers: resolvedTheme === "dark" ? mapLayersDark : mapLayersLight,
+ }
+ return style
+ }, [resolvedTheme])
+
+ const selectHex = useCallback((hexId: string | null) => {
+ if (!hexId) {
+ setSelectedHex(null)
+ return
+ }
+
+ const selectedHex = {
+ hexId,
+ geojson: {
+ type: "MultiPolygon",
+ coordinates: cellsToMultiPolygon([hexId], true),
+ } as GeoJSON.Geometry,
+ }
+
+ setSelectedHex(selectedHex)
+
+ if (!mapRef.current) return
+ const map = mapRef.current.getMap()
+ const [lat, lng] = cellToLatLng(hexId)
+ const bounds = map.getBounds()
+ const zoom = map.getZoom()
+ const hexResolution = getResolution(hexId)
+ const newZoom = ZOOM_BY_HEX_RESOLUTION[hexResolution]
+ if (zoom < newZoom - 3 || !bounds.contains([lng, lat])) {
+ // Fly to the hex if it's not visible in the current viewport, or if it's not zoomed in enough
+ map.flyTo({ center: [lng, lat], zoom: newZoom })
+ }
+ }, [])
+
+ const selectHexByPathname = useCallback(() => {
+ if (!mapRef.current) return
+
+ if (segments.length === 2 && segments[0] === "hex") {
+ const hexId = segments[1]
+ if (selectedHex?.hexId !== hexId) {
+ selectHex(hexId)
+ }
+ } else if (pathname === "/" && selectedHex?.hexId) {
+ selectHex(null)
+ }
+ }, [pathname, segments, selectHex, selectedHex?.hexId])
+
+ useEffect(() => {
+ selectHexByPathname()
+ }, [selectHexByPathname])
+
+ const onClick = useCallback(
+ (event: MapLayerMouseEvent) => {
+ event.features?.forEach(({ layer, properties }) => {
+ if (layer.id !== "hexes_layer" || !properties?.id) return
+ if (selectedHex?.hexId === properties.id) {
+ router.push("/")
+ } else {
+ router.push(`/hex/${properties.id}`)
+ }
+ })
+ },
+ [router, selectedHex?.hexId]
+ )
+
+ useEffect(() => {
+ gaEvent({ action: "map_load" })
+ }, [])
+
+ const onMouseEnter = useCallback(() => setCursor("pointer"), [])
+ const onMouseLeave = useCallback(() => setCursor(""), [])
+
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/HotspotsMap/mapLayersDark.tsx b/src/app/new/HotspotsMap/mapLayersDark.tsx
new file mode 100644
index 0000000..4691d16
--- /dev/null
+++ b/src/app/new/HotspotsMap/mapLayersDark.tsx
@@ -0,0 +1,665 @@
+import { AnyLayer } from "react-map-gl"
+
+export const mapLayersDark: AnyLayer[] = [
+ {
+ id: "background",
+ type: "background",
+ paint: {
+ "background-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "earth",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "earth",
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "landuse_park",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "park"], ["==", "landuse", "cemetery"]],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "landuse_hospital",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "hospital"]],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "landuse_industrial",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "industrial"]],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "landuse_school",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "school"]],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "natural_wood",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "natural",
+ filter: [
+ "any",
+ ["==", "natural", "wood"],
+ ["==", "leisure", "nature_reserve"],
+ ["==", "landuse", "forest"],
+ ],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "landuse_pedestrian",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "highway", "footway"]],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "natural_glacier",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "natural",
+ filter: ["==", "natural", "glacier"],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "natural_sand",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "natural",
+ filter: ["==", "natural", "sand"],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "landuse_aerodrome",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["==", "aeroway", "aerodrome"],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "transit_runway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "transit",
+ filter: ["has", "aeroway"],
+ paint: {
+ "line-color": "#2A2A2A",
+ "line-width": 6,
+ },
+ },
+ {
+ id: "landuse_runway",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: [
+ "any",
+ ["==", "aeroway", "runway"],
+ ["==", "area:aeroway", "runway"],
+ ["==", "area:aeroway", "taxiway"],
+ ],
+ paint: {
+ "fill-color": "#2A2A2A",
+ },
+ },
+ {
+ id: "water",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "water",
+ paint: {
+ "fill-color": "#202020",
+ },
+ },
+ {
+ id: "roads_tunnels_other",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "other"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-dasharray": [1, 1],
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 14,
+ 0,
+ 14.5,
+ 0.5,
+ 20,
+ 12,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_minor",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 12,
+ 0,
+ 12.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_medium",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_major",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "major_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 19,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_highway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 3,
+ 0,
+ 3.5,
+ 0.5,
+ 18,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "physical_line_waterway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "physical_line",
+ filter: ["==", ["get", "pmap:kind"], "waterway"],
+ paint: {
+ "line-color": "#202020",
+ "line-width": 0.5,
+ },
+ },
+ {
+ id: "roads_other",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "other"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-dasharray": [2, 1],
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 14,
+ 0,
+ 14.5,
+ 0.5,
+ 20,
+ 12,
+ ],
+ },
+ },
+ {
+ id: "roads_minor",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 12,
+ 0,
+ 12.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_medium",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: [
+ "all",
+ ["==", "pmap:level", 0],
+ ["==", "pmap:kind", "medium_road"],
+ ],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_major",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "major_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 19,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_highway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 3,
+ 0,
+ 3.5,
+ 0.5,
+ 18,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "transit_railway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "transit",
+ filter: ["all", ["==", "pmap:kind", "railway"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": 2,
+ },
+ },
+ {
+ id: "transit_railway_tracks",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "transit",
+ filter: ["all", ["==", "pmap:kind", "railway"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": 0.8,
+ "line-dasharray": [6, 10],
+ },
+ },
+ {
+ id: "boundaries_country",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "boundaries",
+ filter: ["<=", "pmap:min_admin_level", 2],
+ paint: {
+ "line-color": "#696969",
+ "line-width": 1.5,
+ },
+ },
+ {
+ id: "boundaries",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "boundaries",
+ filter: [">", "pmap:min_admin_level", 2],
+ paint: {
+ "line-color": "#696969",
+ "line-width": 1,
+ "line-dasharray": [1, 2],
+ },
+ },
+ {
+ id: "roads_bridges_other",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "other"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-dasharray": [2, 1],
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 14,
+ 0,
+ 14.5,
+ 0.5,
+ 20,
+ 12,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_minor",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 12,
+ 0,
+ 12.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_medium",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_major",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "major_road"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 19,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_highway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"]],
+ paint: {
+ "line-color": "#3D3D3D",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 3,
+ 0,
+ 3.5,
+ 0.5,
+ 18,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "physical_line_waterway_label",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "physical_line",
+ minzoom: 14,
+ layout: {
+ "symbol-placement": "line",
+ "text-font": ["NotoSans-Regular"],
+ "text-field": ["get", "name"],
+ "text-size": 10,
+ "text-letter-spacing": 0.3,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#151515",
+ "text-halo-width": 1,
+ },
+ },
+ {
+ id: "roads_labels",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "roads",
+ layout: {
+ "symbol-placement": "line",
+ "text-font": ["NotoSans-Regular"],
+ "text-field": ["get", "name"],
+ "text-size": 12,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#151515",
+ "text-halo-width": 2,
+ },
+ },
+ {
+ id: "mask",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "mask",
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "physical_point_ocean",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "physical_point",
+ filter: ["any", ["==", "place", "sea"], ["==", "place", "ocean"]],
+ layout: {
+ "text-font": ["NotoSans-Regular"],
+ "text-field": ["get", "name"],
+ "text-size": 13,
+ "text-letter-spacing": 0.2,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ },
+ },
+ {
+ id: "places_subplace",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "pmap:kind", "neighbourhood"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": 10,
+ "text-transform": "uppercase",
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#151515",
+ "text-halo-width": 0.5,
+ },
+ },
+ {
+ id: "places_city",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "pmap:kind", "city"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": ["step", ["get", "pmap:rank"], 0, 1, 12, 2, 10],
+ "text-variable-anchor": ["bottom-left"],
+ "text-radial-offset": 0.2,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#151515",
+ "text-halo-width": 1,
+ },
+ },
+ {
+ id: "places_state",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "pmap:kind", "state"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": 14,
+ "text-radial-offset": 0.2,
+ "text-anchor": "center",
+ "text-transform": "uppercase",
+ "text-letter-spacing": 0.1,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#151515",
+ "text-halo-width": 1,
+ },
+ },
+ {
+ id: "places_country",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "place", "country"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": 10,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#151515",
+ "text-halo-width": 1,
+ },
+ },
+]
diff --git a/src/app/new/HotspotsMap/mapLayersLight.tsx b/src/app/new/HotspotsMap/mapLayersLight.tsx
new file mode 100644
index 0000000..6981b29
--- /dev/null
+++ b/src/app/new/HotspotsMap/mapLayersLight.tsx
@@ -0,0 +1,665 @@
+import { AnyLayer } from "react-map-gl"
+
+export const mapLayersLight: AnyLayer[] = [
+ {
+ id: "background",
+ type: "background",
+ paint: {
+ "background-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "earth",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "earth",
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "landuse_park",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "park"], ["==", "landuse", "cemetery"]],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "landuse_hospital",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "hospital"]],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "landuse_industrial",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "industrial"]],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "landuse_school",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "pmap:kind", "school"]],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "natural_wood",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "natural",
+ filter: [
+ "any",
+ ["==", "natural", "wood"],
+ ["==", "leisure", "nature_reserve"],
+ ["==", "landuse", "forest"],
+ ],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "landuse_pedestrian",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["any", ["==", "highway", "footway"]],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "natural_glacier",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "natural",
+ filter: ["==", "natural", "glacier"],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "natural_sand",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "natural",
+ filter: ["==", "natural", "sand"],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "landuse_aerodrome",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: ["==", "aeroway", "aerodrome"],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "transit_runway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "transit",
+ filter: ["has", "aeroway"],
+ paint: {
+ "line-color": "#F3F3F1",
+ "line-width": 6,
+ },
+ },
+ {
+ id: "landuse_runway",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "landuse",
+ filter: [
+ "any",
+ ["==", "aeroway", "runway"],
+ ["==", "area:aeroway", "runway"],
+ ["==", "area:aeroway", "taxiway"],
+ ],
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "water",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "water",
+ paint: {
+ "fill-color": "#CAD2D3",
+ },
+ },
+ {
+ id: "roads_tunnels_other",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "other"]],
+ paint: {
+ "line-color": "#fff",
+ "line-dasharray": [1, 1],
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 14,
+ 0,
+ 14.5,
+ 0.5,
+ 20,
+ 12,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_minor",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 12,
+ 0,
+ 12.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_medium",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_major",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "major_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 19,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_tunnels_highway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 3,
+ 0,
+ 3.5,
+ 0.5,
+ 18,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "physical_line_waterway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "physical_line",
+ filter: ["==", ["get", "pmap:kind"], "waterway"],
+ paint: {
+ "line-color": "#fff",
+ "line-width": 0.5,
+ },
+ },
+ {
+ id: "roads_other",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "other"]],
+ paint: {
+ "line-color": "#fff",
+ "line-dasharray": [2, 1],
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 14,
+ 0,
+ 14.5,
+ 0.5,
+ 20,
+ 12,
+ ],
+ },
+ },
+ {
+ id: "roads_minor",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 12,
+ 0,
+ 12.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_medium",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: [
+ "all",
+ ["==", "pmap:level", 0],
+ ["==", "pmap:kind", "medium_road"],
+ ],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_major",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "major_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 19,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_highway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 3,
+ 0,
+ 3.5,
+ 0.5,
+ 18,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "transit_railway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "transit",
+ filter: ["all", ["==", "pmap:kind", "railway"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": 2,
+ },
+ },
+ {
+ id: "transit_railway_tracks",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "transit",
+ filter: ["all", ["==", "pmap:kind", "railway"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": 0.8,
+ "line-dasharray": [6, 10],
+ },
+ },
+ {
+ id: "boundaries_country",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "boundaries",
+ filter: ["<=", "pmap:min_admin_level", 2],
+ paint: {
+ "line-color": "#9e9e9e",
+ "line-width": 1,
+ },
+ },
+ {
+ id: "boundaries",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "boundaries",
+ filter: [">", "pmap:min_admin_level", 2],
+ paint: {
+ "line-color": "#9e9e9e",
+ "line-width": 1,
+ "line-dasharray": [1, 2],
+ },
+ },
+ {
+ id: "roads_bridges_other",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "other"]],
+ paint: {
+ "line-color": "#fff",
+ "line-dasharray": [2, 1],
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 14,
+ 0,
+ 14.5,
+ 0.5,
+ 20,
+ 12,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_minor",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "minor_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 12,
+ 0,
+ 12.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_medium",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "medium_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 20,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_major",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "major_road"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 7,
+ 0,
+ 7.5,
+ 0.5,
+ 19,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "roads_bridges_highway",
+ type: "line",
+ source: "protomaps",
+ "source-layer": "roads",
+ filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"]],
+ paint: {
+ "line-color": "#fff",
+ "line-width": [
+ "interpolate",
+ ["exponential", 1.6],
+ ["zoom"],
+ 3,
+ 0,
+ 3.5,
+ 0.5,
+ 18,
+ 32,
+ ],
+ },
+ },
+ {
+ id: "physical_line_waterway_label",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "physical_line",
+ minzoom: 14,
+ layout: {
+ "symbol-placement": "line",
+ "text-font": ["NotoSans-Regular"],
+ "text-field": ["get", "name"],
+ "text-size": 10,
+ "text-letter-spacing": 0.3,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#fff",
+ "text-halo-width": 1,
+ },
+ },
+ {
+ id: "roads_labels",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "roads",
+ layout: {
+ "symbol-placement": "line",
+ "text-font": ["NotoSans-Regular"],
+ "text-field": ["get", "name"],
+ "text-size": 12,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#fff",
+ "text-halo-width": 2,
+ },
+ },
+ {
+ id: "mask",
+ type: "fill",
+ source: "protomaps",
+ "source-layer": "mask",
+ paint: {
+ "fill-color": "#F3F3F1",
+ },
+ },
+ {
+ id: "physical_point_ocean",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "physical_point",
+ filter: ["any", ["==", "place", "sea"], ["==", "place", "ocean"]],
+ layout: {
+ "text-font": ["NotoSans-Regular"],
+ "text-field": ["get", "name"],
+ "text-size": 13,
+ "text-letter-spacing": 0.2,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ },
+ },
+ {
+ id: "places_subplace",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "pmap:kind", "neighbourhood"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": 10,
+ "text-transform": "uppercase",
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#fff",
+ "text-halo-width": 0.5,
+ },
+ },
+ {
+ id: "places_city",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "pmap:kind", "city"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": ["step", ["get", "pmap:rank"], 0, 1, 12, 2, 10],
+ "text-variable-anchor": ["bottom-left"],
+ "text-radial-offset": 0.2,
+ },
+ paint: {
+ "text-color": "#6D6D6D",
+ "text-halo-color": "#fff",
+ "text-halo-width": 1,
+ },
+ },
+ {
+ id: "places_state",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "pmap:kind", "state"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": 14,
+ "text-radial-offset": 0.2,
+ "text-anchor": "center",
+ "text-transform": "uppercase",
+ "text-letter-spacing": 0.1,
+ },
+ paint: {
+ "text-color": "#A8A8A8",
+ "text-halo-color": "#fff",
+ "text-halo-width": 1,
+ },
+ },
+ {
+ id: "places_country",
+ type: "symbol",
+ source: "protomaps",
+ "source-layer": "places",
+ filter: ["==", "place", "country"],
+ layout: {
+ "text-field": "{name:en}",
+ "text-font": ["NotoSans-Regular"],
+ "text-size": 10,
+ },
+ paint: {
+ "text-color": "#848484",
+ "text-halo-color": "#fff",
+ "text-halo-width": 1,
+ },
+ },
+]
diff --git a/src/app/new/HotspotsMap/utils.ts b/src/app/new/HotspotsMap/utils.ts
new file mode 100644
index 0000000..cac37aa
--- /dev/null
+++ b/src/app/new/HotspotsMap/utils.ts
@@ -0,0 +1,138 @@
+import { HeliumIotIcon } from "@/components/icons/HeliumIotIcon"
+import { HeliumMobileIcon } from "@/components/icons/HeliumMobileIcon"
+import { CoordPair } from "h3-js"
+
+export const MIN_MAP_ZOOM = 2
+export const MAX_MAP_ZOOM = 14
+
+const WORLD_BOUNDS: [CoordPair, CoordPair] = [
+ [-134.827109, 57.785781],
+ [129.767893, -30.955724],
+]
+
+export const INITIAL_MAP_VIEW_STATE = {
+ bounds: WORLD_BOUNDS,
+}
+
+export const MAP_CONTAINER_STYLE: React.CSSProperties = {
+ height: "100%",
+ width: "100%",
+ overflow: "hidden",
+ position: "relative",
+ backgroundColor: "rgb(19,24,37)",
+}
+
+export const MIN_HEXES_ZOOM = 7
+export const MIN_HEX_LABELS_ZOOM = 11
+export const POINTS_AND_HEXES_OVERLAP = 2
+
+export const HELIUM_IOT_COLOR = "#27EE76"
+export const HELIUM_MOBILE_COLOR = "#009FF9"
+
+export const getHexFillStyle = (color: string): mapboxgl.FillPaint => ({
+ "fill-color": color,
+ "fill-opacity": 0.4,
+})
+
+export const getBlurredPointStyle = (color: string): mapboxgl.CirclePaint => ({
+ "circle-color": color,
+ "circle-opacity": [
+ "interpolate",
+ ["exponential", 2],
+ ["zoom"],
+ MIN_MAP_ZOOM,
+ 0.05,
+ MIN_HEXES_ZOOM + POINTS_AND_HEXES_OVERLAP,
+ 0.4,
+ ],
+ "circle-radius": [
+ "interpolate",
+ ["exponential", 2],
+ ["zoom"],
+ MIN_MAP_ZOOM,
+ 3,
+ MIN_HEXES_ZOOM + POINTS_AND_HEXES_OVERLAP,
+ 2,
+ ],
+})
+
+export const getHexOutlineStyle = (
+ theme: string | undefined
+): mapboxgl.LinePaint => ({
+ "line-color": theme === "dark" ? "#fff" : "rgb(113,113,122)",
+ "line-width": 4,
+})
+
+export const getHexLabelStyle = (
+ theme: string | undefined
+): mapboxgl.SymbolPaint => ({
+ "text-color": theme === "dark" ? "white" : "#6D6D6D",
+})
+
+export const hexLabelLayout: mapboxgl.SymbolLayout = {
+ "text-font": ["NotoSans-Regular"],
+ "text-field": ["get", "count"],
+ "text-allow-overlap": false,
+ "text-size": 23,
+}
+
+export interface HexFeatureDetails {
+ hexId: string
+ geojson: GeoJSON.Geometry
+}
+
+export const ZOOM_BY_HEX_RESOLUTION: { [resolution: number]: number } = {
+ 10: 14,
+ 9: 14,
+ 8: 13,
+ 7: 12,
+ 6: 11,
+ 5: 10,
+ 4: 9,
+}
+
+interface LayerConfig {
+ sourcePath: string
+ sourceLayer: string
+}
+
+export interface NetworkCoverageLayerOption {
+ name: string
+ icon: (props: any) => JSX.Element
+ color: string
+ sourceDomain: string
+ points: LayerConfig
+ hexes: LayerConfig
+}
+
+export const networkLayers: { [network: string]: NetworkCoverageLayerOption } =
+ {
+ mobile: {
+ name: "MOBILE",
+ icon: HeliumMobileIcon,
+ color: HELIUM_MOBILE_COLOR,
+ sourceDomain: process.env.NEXT_PUBLIC_HOTSPOTTY_TILESERVER_URL!,
+ points: {
+ sourcePath: "public.helium_mobile_points.json",
+ sourceLayer: "public.helium_mobile_points",
+ },
+ hexes: {
+ sourcePath: "public.helium_mobile_hexes.json",
+ sourceLayer: "public.helium_mobile_hexes",
+ },
+ },
+ iot: {
+ name: "IOT",
+ icon: HeliumIotIcon,
+ color: HELIUM_IOT_COLOR,
+ sourceDomain: process.env.NEXT_PUBLIC_HOTSPOTTY_TILESERVER_URL!,
+ points: {
+ sourcePath: "public.helium_iot_points.json",
+ sourceLayer: "public.helium_iot_points",
+ },
+ hexes: {
+ sourcePath: "public.helium_iot_hexes.json",
+ sourceLayer: "public.helium_iot_hexes",
+ },
+ },
+ }
diff --git a/src/app/new/Nav/Nav.module.css b/src/app/new/Nav/Nav.module.css
new file mode 100644
index 0000000..71185b8
--- /dev/null
+++ b/src/app/new/Nav/Nav.module.css
@@ -0,0 +1,22 @@
+/* Required for the nested blurs we get when using the selector. See https://stackoverflow.com/questions/60997948/backdrop-filter-not-working-for-nested-elements-in-chrome */
+.blur::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ -webkit-backdrop-filter: blur(8px);
+ backdrop-filter: blur(8px);
+ top: 0px;
+ left: 0px;
+ border-radius: 12px;
+ z-index: -1;
+}
+
+.wrapper {
+ scrollbar-gutter: stable;
+ scrollbar-color: #404040b3 transparent; /*firefox*/
+}
+.wrapper::-webkit-scrollbar {
+ border: none;
+ background-color: transparent;
+}
diff --git a/src/app/new/Nav/Nav.tsx b/src/app/new/Nav/Nav.tsx
new file mode 100644
index 0000000..edbc1bb
--- /dev/null
+++ b/src/app/new/Nav/Nav.tsx
@@ -0,0 +1,140 @@
+"use client"
+
+import { Coverage } from "@/components/icons/Coverage"
+import { EnergyIcon } from "@/components/icons/EnergyIcon"
+import { HeliumIcon2 } from "@/components/icons/HeliumIcon2"
+import { IotIcon } from "@/components/icons/IotIcon"
+import { MobileIcon } from "@/components/icons/MobileIcon"
+import { RssiPill } from "@/components/shared/RssiPill"
+import clsx from "clsx"
+import Link from "next/link"
+import { useCallback, useState } from "react"
+import styles from "./Nav.module.css"
+import { Search } from "./Search"
+import { Selector } from "./Selector"
+
+const Logo = () => {
+ return (
+
+ )
+}
+
+const NETWORKS = [
+ {
+ Icon: ,
+ name: "IOT",
+ },
+ {
+ Icon: ,
+ name: "MOBILE",
+ },
+ {
+ Icon: (
+
+
+
+ ),
+ name: "ENERGY",
+ },
+]
+
+const MAP_SETTINGS = [
+ { name: "Modeled Coverage", Icon: },
+ { name: "Hotspot Location", Icon: },
+]
+
+const Divider = () =>
+type Selectors = "" | "map_settings" | "networks"
+
+export const Nav = () => {
+ const [openSelector, setOpenSelector] = useState("")
+ const toggleSelector = useCallback((selectorLabel: Selectors) => {
+ return () =>
+ setOpenSelector((currentOpenSelector) => {
+ if (currentOpenSelector === selectorLabel) return ""
+ else return selectorLabel
+ })
+ }, [])
+
+ return (
+
+ )
+}
diff --git a/src/app/new/Nav/Search.tsx b/src/app/new/Nav/Search.tsx
new file mode 100644
index 0000000..b86dc0f
--- /dev/null
+++ b/src/app/new/Nav/Search.tsx
@@ -0,0 +1,249 @@
+"use client"
+
+import { HeliumIotIcon } from "@/components/icons/HeliumIotIcon"
+import { HeliumMobileIcon } from "@/components/icons/HeliumMobileIcon"
+import { LoadingIcon } from "@/components/icons/LoadingIcon"
+import { Combobox, Dialog, Transition } from "@headlessui/react"
+import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"
+import { QuestionMarkCircleIcon } from "@heroicons/react/24/solid"
+import clsx from "clsx"
+import { isValidCell } from "h3-js"
+import { useRouter } from "next/navigation"
+import { Fragment, useCallback, useState } from "react"
+import { useDebouncedCallback } from "use-debounce"
+import styles from "./Nav.module.css"
+
+let controller: AbortController | null = null
+
+const RESULTS_LIMIT = 20
+
+type SearchResponse = {
+ result: {
+ data: {
+ json: HotspotResult[]
+ }
+ }
+}
+export interface HotspotResult {
+ address: string
+ hotspotType: string
+ name: string
+ statusString: "inactive" | "active"
+ location: {
+ hex: string
+ }
+}
+
+export function Search() {
+ const router = useRouter()
+ const [query, setQuery] = useState("")
+ const [open, setOpen] = useState(false)
+ const [searchResults, setSearchResults] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+
+ const searchItemByQuery = useCallback(async (query: string) => {
+ if (controller) controller.abort()
+
+ controller = new AbortController()
+ const signal = controller.signal
+
+ setSearchResults([])
+
+ if (!query) return null
+
+ setIsLoading(true)
+
+ try {
+ const queryObj = {
+ json: {
+ name: query,
+ },
+ }
+ const jsonString = JSON.stringify(queryObj)
+ const urlEncoded = encodeURIComponent(jsonString)
+ const queryString = `?input=${urlEncoded}`
+ const searchUrl = new URL(
+ `${process.env.NEXT_PUBLIC_HELIUMGEEK_EXPLORER_API_URL}${queryString}`
+ )
+
+ const response = (await fetch(searchUrl, {
+ signal,
+ next: { revalidate: 10 },
+ }).then((res) => res.json())) as SearchResponse
+
+ setSearchResults(response.result.data.json)
+ setIsLoading(false)
+ } catch {
+ setIsLoading(false)
+ }
+ }, [])
+
+ const onChangeSearch = useDebouncedCallback((query: string) => {
+ if (query.length === 15 && isValidCell(query)) {
+ router.push(`/hex/${query}`)
+ setOpen(false)
+ clearSearchModal()
+ } else {
+ searchItemByQuery(query)
+ }
+ }, 400)
+
+ const clearSearchModal = useCallback(() => {
+ setQuery("")
+ setSearchResults([])
+ setIsLoading(false)
+ }, [])
+
+ const handleHotspotSelection = useCallback(
+ (hotspot: HotspotResult) => {
+ router.push(
+ `/new/hex/${hotspot.location.hex}/hotspots/${hotspot.address}`
+ )
+ setOpen(false)
+ },
+ [router]
+ )
+
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/src/app/new/Nav/Selector.tsx b/src/app/new/Nav/Selector.tsx
new file mode 100644
index 0000000..fef33ba
--- /dev/null
+++ b/src/app/new/Nav/Selector.tsx
@@ -0,0 +1,65 @@
+import { ChevronDownIcon } from "@heroicons/react/24/outline"
+import clsx from "clsx"
+import { ReactElement, useState } from "react"
+import styles from "./Nav.module.css"
+
+type Option = {
+ Icon?: ReactElement
+ name: string
+}
+
+export const Selector = ({
+ options,
+ width,
+ isOpen,
+ toggle,
+}: {
+ options: Option[]
+ width: string
+ isOpen: boolean
+ toggle: () => void
+}) => {
+ const [selected, setSelected] = useState(options[0])
+
+ return (
+
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/app/new/Settings/HotspotProviders.tsx b/src/app/new/Settings/HotspotProviders.tsx
new file mode 100644
index 0000000..e2d24e2
--- /dev/null
+++ b/src/app/new/Settings/HotspotProviders.tsx
@@ -0,0 +1,114 @@
+import { RadioCircles } from "@/components/shared/RadioCircles"
+import { PROVIDERS, usePreferences } from "@/context/usePreferences"
+import clsx from "clsx"
+import { useMemo } from "react"
+
+export type Provider = {
+ Icon: JSX.Element
+ label: string
+ getUrl: (hotspotId: string) => string
+}
+
+export const NO_PREFERENCE: Provider = {
+ Icon: (
+
+ ),
+ label: "Ask me before opening",
+ getUrl: () => "",
+}
+
+type HotspotProvidersProps = {
+ address?: string
+ close?: () => void
+}
+
+export const HotspotProviders = ({ address, close }: HotspotProvidersProps) => {
+ const { provider, setProvider, savePreference, setSavePreference } =
+ usePreferences()
+ const providers = useMemo(() => {
+ const providers = [...PROVIDERS]
+ if (!address) providers.push(NO_PREFERENCE)
+ return providers
+ }, [address])
+
+ return (
+
+
+ Select a Third-Party Explorer
+
+
+ {!!address && (
+ <>
+
+ You will be redirected to a external page to Helium if you need
+ more information about the Hotspot.
+
+
+ >
+ )}
+
+ {providers.map((providerItem) => {
+ const { label, Icon } = providerItem
+ const isActive = provider?.label === label
+ return (
+
+ )
+ })}
+
+ {!!address && (
+
+ You can change this selection from Settings.
+
+ )}
+
+
+ )
+}
diff --git a/src/app/new/Settings/Settings.tsx b/src/app/new/Settings/Settings.tsx
new file mode 100644
index 0000000..478ca0a
--- /dev/null
+++ b/src/app/new/Settings/Settings.tsx
@@ -0,0 +1,45 @@
+import { ChevronLeftIcon, XMarkIcon } from "@heroicons/react/24/outline"
+import clsx from "clsx"
+import { useState } from "react"
+import { HotspotProviders } from "./HotspotProviders"
+import { SettingsMain } from "./SettingsMain"
+import { ThemeToggle } from "./ThemeToggle"
+
+export const Settings = ({ close }: { close: () => void }) => {
+ const [setting, setSetting] = useState("main")
+ return (
+
+ <>
+
+ {setting === "main" && (
+
+ )}
+ {setting !== "main" && (
+
+ )}
+
+ {setting === "main" && (
+
+ )}
+ {setting === "provider" &&
}
+ {setting === "theme" &&
}
+ >
+
+ )
+}
diff --git a/src/app/new/Settings/SettingsMain.tsx b/src/app/new/Settings/SettingsMain.tsx
new file mode 100644
index 0000000..ed3513a
--- /dev/null
+++ b/src/app/new/Settings/SettingsMain.tsx
@@ -0,0 +1,57 @@
+import {
+ ArrowTopRightOnSquareIcon,
+ ChevronRightIcon,
+ Cog6ToothIcon,
+ MoonIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline"
+
+export const SettingsMain = ({
+ close,
+ setSetting,
+}: {
+ close: () => void
+ setSetting: (settingValue: string) => void
+}) => {
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/Settings/ThemeToggle.tsx b/src/app/new/Settings/ThemeToggle.tsx
new file mode 100644
index 0000000..b141ce2
--- /dev/null
+++ b/src/app/new/Settings/ThemeToggle.tsx
@@ -0,0 +1,62 @@
+"use client"
+
+import { RadioCircles } from "@/components/shared/RadioCircles"
+import {
+ ComputerDesktopIcon,
+ MoonIcon,
+ SunIcon,
+} from "@heroicons/react/24/outline"
+import clsx from "clsx"
+import { useTheme } from "next-themes"
+
+const THEMES = [
+ {
+ label: "Toggle dark mode",
+ value: "dark",
+ Icon: ,
+ },
+ {
+ label: "Toggle light mode",
+ value: "light",
+ Icon: ,
+ },
+ {
+ label: "Toggle browser settings",
+ value: "system",
+ Icon: ,
+ },
+]
+
+export const ThemeToggle = () => {
+ const { theme, setTheme } = useTheme()
+ return (
+
+
Select a mode
+ {THEMES.map(({ label, Icon, value }) => {
+ const isActive = value === theme
+ return (
+
+ )
+ })}
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/HexPage.tsx b/src/app/new/hex/[hex]/HexPage.tsx
new file mode 100644
index 0000000..dead031
--- /dev/null
+++ b/src/app/new/hex/[hex]/HexPage.tsx
@@ -0,0 +1,121 @@
+"use client"
+
+import { HexOutlineIcon } from "@/components/icons/HexOutlineIcon"
+import { InfoCard } from "@/components/shared/InfoCard"
+import { useInfoWrapper } from "@/components/shared/InfoWrapper"
+import { XMarkIcon } from "@heroicons/react/24/outline"
+import KuzcoLogo from "@public/kuzco-logo.png"
+import Image from "next/image"
+import Link from "next/link"
+import { RssiCoverage } from "./RssiCoverage"
+import { RssiHotspot, RssiHotspotList } from "./RssiHotspotList"
+import { RssiOverview } from "./RssiOverview"
+
+const HOTSPOTS: RssiHotspot[] = [
+ {
+ address: "112SFacmbDiWrDfXPoZVAd3od9BbbHMFxQhDWNyrgU4KKfGK6gtG",
+ rssi: 91,
+ },
+ {
+ address: "112VZR2pe4APgF3Gb78KjaJmUhaE8PPgp1cpB1q2hWq6Mt2GdMbb",
+ rssi: 85,
+ },
+ {
+ address: "11WhQN8PLb6FFkmgSir3Kosw2pVp1vrRtxgh6aF7UYbHYSPbzVB",
+ rssi: 84,
+ },
+ {
+ address: "112LGVmE97oPdyYbdCGadwDavT7BBH8EjNpEzST8VrXqHEK6btQt",
+ rssi: 69,
+ },
+ {
+ address: "115mA2TtBNSLgw5ap94Ku19gVTnNzRupcc144f5QJ7qjFAYNVFx",
+ rssi: 52,
+ },
+ {
+ address: "115mA2TtBNSLgw5ap94Ku19gVTnNzRupcc144f5QJ7qjFAYNVFx",
+ rssi: 44,
+ },
+ {
+ address: "112c1ffCAmNcd7PxSyg9N2e1dzvdRYw7SnH1uQUBcTp54BJAtaW5",
+ rssi: 39,
+ },
+]
+
+const getHotspotsInfo = (hotspots: RssiHotspot[]) => {
+ const info = {
+ max: 0,
+ strong: 0,
+ medium: 0,
+ low: 0,
+ }
+
+ hotspots.forEach(({ rssi }) => {
+ info.max = Math.max(info.max, rssi)
+ if (rssi >= 90) info.strong++
+ else if (rssi >= 70) info.medium++
+ else info.low++
+ })
+
+ return info
+}
+
+const Divider = () => (
+
+)
+
+type Params = {
+ hex: string
+}
+
+export const HexPage = ({ params }: { params: Params }) => {
+ const hotspotsInfo = getHotspotsInfo(HOTSPOTS)
+ const { isOpen } = useInfoWrapper()
+
+ return (
+
+
+ {!isOpen && (
+
+
+
+ )}
+
+ {!isOpen && (
+
+
+
+ )}
+
+
+
+ Arroyo Grande is a city in San Luis Obispo County, California, United
+ States. Its population is 15,851 inhabitants according to the 2000
+ census.
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/RssiCoverage.module.css b/src/app/new/hex/[hex]/RssiCoverage.module.css
new file mode 100644
index 0000000..f5581b5
--- /dev/null
+++ b/src/app/new/hex/[hex]/RssiCoverage.module.css
@@ -0,0 +1,19 @@
+.lowInset {
+ margin-left: -6px;
+ background-image: radial-gradient(
+ circle at 5px 6px,
+ rgba(0, 0, 0, 0) 0,
+ rgba(0, 0, 0, 0) 6px,
+ #01fff0 6px
+ );
+}
+
+.mediumInset {
+ margin-left: -6px;
+ background-image: radial-gradient(
+ circle at 5px 6px,
+ rgba(0, 0, 0, 0) 0,
+ rgba(0, 0, 0, 0) 6px,
+ #ffd600 6px
+ );
+}
diff --git a/src/app/new/hex/[hex]/RssiCoverage.tsx b/src/app/new/hex/[hex]/RssiCoverage.tsx
new file mode 100644
index 0000000..ef176eb
--- /dev/null
+++ b/src/app/new/hex/[hex]/RssiCoverage.tsx
@@ -0,0 +1,86 @@
+import { RssiPill, getRssiColor } from "@/components/shared/RssiPill"
+import clsx from "clsx"
+import styles from "./RssiCoverage.module.css"
+
+type RssiCoverageProps = {
+ strong: number
+ medium: number
+ low: number
+}
+
+export const RssiCoverage = ({ strong, medium, low }: RssiCoverageProps) => {
+ const isMediumInset = !!strong
+ const isLowInset = !!strong || !!medium
+
+ return (
+
+
+ Signal Strength Distribution
+
+
+ {!!strong && (
+
+ )}
+ {!!medium && (
+
+ )}
+ {!!low && (
+
+ )}
+
+
+
+
+
+
+
+
-90 dBm
+
+ {strong} Hotspot
+
+
+
+
+
+
+
+
+
-70 dBm
+
+ {medium} Hotspots
+
+
+
+
+
+
+
+
+
-50 dBm
+
+ {low} Hotspots
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/RssiHotspotList.tsx b/src/app/new/hex/[hex]/RssiHotspotList.tsx
new file mode 100644
index 0000000..760da89
--- /dev/null
+++ b/src/app/new/hex/[hex]/RssiHotspotList.tsx
@@ -0,0 +1,44 @@
+import { RssiPill } from "@/components/shared/RssiPill"
+import animalHash from "angry-purple-tiger"
+import Link from "next/link"
+
+export type RssiHotspot = {
+ address: string
+ rssi: number
+}
+
+type RssiHotspotListProps = {
+ hotspots: RssiHotspot[]
+ hex: string
+}
+
+export const RssiHotspotList = ({ hotspots, hex }: RssiHotspotListProps) => {
+ return (
+
+
Hotspots
+
+ {hotspots.map(({ address, rssi }) => {
+ return (
+
+
+
+ {animalHash(address)}
+
+
+
+
+ RSSI: -{rssi} dBm
+
+
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/RssiOverview.tsx b/src/app/new/hex/[hex]/RssiOverview.tsx
new file mode 100644
index 0000000..894a571
--- /dev/null
+++ b/src/app/new/hex/[hex]/RssiOverview.tsx
@@ -0,0 +1,60 @@
+import { RssiPill } from "@/components/shared/RssiPill"
+import clsx from "clsx"
+
+type RssiOverviewProps = {
+ max: number
+ isSmall: boolean
+}
+
+export const RssiOverview = ({ max, isSmall }: RssiOverviewProps) => {
+ return (
+
+ {!isSmall && (
+
+
+ Hotspots in This Area
+
+
4
+
+ )}
+
+
+ Max Expected Signal Strength
+
+
+
+
+
+ -{max}
+
+
+ {isSmall ? " " : ""}dBm
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/hotspots/[address]/ConnectedDevices.tsx b/src/app/new/hex/[hex]/hotspots/[address]/ConnectedDevices.tsx
new file mode 100644
index 0000000..0e290fa
--- /dev/null
+++ b/src/app/new/hex/[hex]/hotspots/[address]/ConnectedDevices.tsx
@@ -0,0 +1,94 @@
+"use client"
+
+import { InfoCard, InfoCardBody } from "@/components/shared/InfoCard"
+import { RssiPill } from "@/components/shared/RssiPill"
+import ConnectedDevicesIcon from "@public/connected-devices.png"
+import clsx from "clsx"
+import Image from "next/image"
+import { useState } from "react"
+import { useOpenCard } from "./useOpenCard"
+
+const CARD_LABEL = "CONNECTED_DEVICES"
+
+export const ConnectedDevices = () => {
+ const { openCard, setOpenCard } = useOpenCard()
+ const isActive = openCard === CARD_LABEL
+ const [{ mappers, dimo }, setPreferences] = useState({
+ mappers: true,
+ dimo: true,
+ })
+
+ return (
+
+
+
+
+ {isActive && (
+
+
+ setPreferences((preferences) => ({
+ ...preferences,
+ mappers: !preferences.mappers,
+ }))
+ }
+ >
+
+
+
+ setPreferences((preferences) => ({
+ ...preferences,
+ dimo: !preferences.dimo,
+ }))
+ }
+ >
+
+
+
+ )}
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/hotspots/[address]/DetailsHeader.tsx b/src/app/new/hex/[hex]/hotspots/[address]/DetailsHeader.tsx
new file mode 100644
index 0000000..24dbd8a
--- /dev/null
+++ b/src/app/new/hex/[hex]/hotspots/[address]/DetailsHeader.tsx
@@ -0,0 +1,81 @@
+"use client"
+
+import { HexOutlineIcon } from "@/components/icons/HexOutlineIcon"
+import { InfoCard } from "@/components/shared/InfoCard"
+import { ArrowLeftIcon, XMarkIcon } from "@heroicons/react/24/outline"
+import HotspotWaves from "@public/hotspot-waves.png"
+import animalHash from "angry-purple-tiger"
+import clsx from "clsx"
+import Image from "next/image"
+import Link from "next/link"
+import { useRouter } from "next/navigation"
+import { useOpenCard } from "./useOpenCard"
+
+export const metadata = {
+ title: "Helium Hotspots Map - Hotspot Details",
+}
+
+type DetailsHeaderProps = {
+ hex: string
+ address: string
+}
+
+export const DetailsHeaderProps = ({ hex, address }: DetailsHeaderProps) => {
+ const { openCard, setOpenCard } = useOpenCard()
+ const router = useRouter()
+ const isActiveCard = !!openCard
+
+ return (
+
+
+
setOpenCard("")
+ : () => router.push(`/new/hex/${hex}`)
+ }
+ >
+
+
+ {openCard ? "Back to Hotspot details" : "Hex Hotspots list"}
+
+
+
+
+
+
+
+
+
+
+
+
+ {animalHash(address)}
+
+
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/hotspots/[address]/ExplorerOptions.tsx b/src/app/new/hex/[hex]/hotspots/[address]/ExplorerOptions.tsx
new file mode 100644
index 0000000..903b3cd
--- /dev/null
+++ b/src/app/new/hex/[hex]/hotspots/[address]/ExplorerOptions.tsx
@@ -0,0 +1,102 @@
+"use client"
+
+import {
+ HotspotProviders,
+ NO_PREFERENCE,
+} from "@/app/new/Settings/HotspotProviders"
+import { InfoCard } from "@/components/shared/InfoCard"
+import { Overlay } from "@/components/shared/Overlay"
+import { usePreferences } from "@/context/usePreferences"
+import {
+ ArrowTopRightOnSquareIcon,
+ Cog6ToothIcon,
+ XMarkIcon,
+} from "@heroicons/react/24/outline"
+import ConnectedDots from "@public/connected-dots.png"
+import clsx from "clsx"
+import Image from "next/image"
+import Link from "next/link"
+import { useState } from "react"
+
+type ExplorerOptionsType = {
+ address: string
+}
+
+export const ExplorerOptions = ({ address }: ExplorerOptionsType) => {
+ const [isOpen, setIsOpen] = useState(false)
+ const { provider } = usePreferences()
+ const hasNoPreference = provider?.label === NO_PREFERENCE.label
+
+ return (
+
+ {hasNoPreference && (
+ setIsOpen(() => true)}
+ >
+
+
+
+ Open in Third-Party Explorer
+
+
+ You haven't set this up yet
+
+
+
+ )}
+ {!hasNoPreference && (
+
+
+ {provider?.Icon}
+
+ Open with {provider?.label}
+
+
+
setIsOpen(() => true)}
+ >
+
+
+
+ )}
+
+
+
+
+
+ setIsOpen(false)}>
+
+
+
+
setIsOpen(false)}
+ />
+
+
+
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/hotspots/[address]/Insights.tsx b/src/app/new/hex/[hex]/hotspots/[address]/Insights.tsx
new file mode 100644
index 0000000..99f0d83
--- /dev/null
+++ b/src/app/new/hex/[hex]/hotspots/[address]/Insights.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+import { InfoCard, InfoCardBody } from "@/components/shared/InfoCard"
+import { CheckCircleIcon } from "@heroicons/react/24/outline"
+import Hotspot from "@public/hotspot.png"
+import clsx from "clsx"
+import Image from "next/image"
+import { useOpenCard } from "./useOpenCard"
+
+const CARD_LABEL = "INSIGHTS"
+
+export const Insights = () => {
+ const { openCard, setOpenCard } = useOpenCard()
+ const isActive = openCard === CARD_LABEL
+
+ return (
+
+ setOpenCard(CARD_LABEL)}
+ >
+
+
+ Hotspot Insights
+
+
+ {isActive && (
+
+
+ Your device coverage is strong due to the area you are in.
+
+
+
+
+
+
+ This Hotspot has transferred data in the last 30 days.
+
+
+
+
+ )}
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/hotspots/[address]/TechnicalInfo.tsx b/src/app/new/hex/[hex]/hotspots/[address]/TechnicalInfo.tsx
new file mode 100644
index 0000000..e588cc0
--- /dev/null
+++ b/src/app/new/hex/[hex]/hotspots/[address]/TechnicalInfo.tsx
@@ -0,0 +1,103 @@
+"use client"
+
+import { InfoCard, InfoCardBody } from "@/components/shared/InfoCard"
+import LatitudeIcon from "@public/latitude.png"
+import LongitudeIcon from "@public/longitude.png"
+import TechInfoIcon from "@public/tech-info.png"
+import clsx from "clsx"
+import Image from "next/image"
+import { PropsWithChildren } from "react"
+import { useOpenCard } from "./useOpenCard"
+
+const Header = ({ children }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ )
+}
+
+const Body = ({ children }: PropsWithChildren) => {
+ return (
+ {children}
+ )
+}
+
+const CARD_LABEL = "TECHNICAL_INFO"
+
+export const TechnicalInfo = () => {
+ const { openCard, setOpenCard } = useOpenCard()
+ const isActive = openCard === CARD_LABEL
+
+ return (
+
+ setOpenCard(CARD_LABEL)}
+ >
+
+
+ Technical Information
+
+
+ {isActive && (
+
+
+ {/* When we have real data, we'll want to only show hours and seconds if less than a day */}
+
+ 3y 45m 27d 17:34:56
+
+
+
+ Cisco Systems
+
+
+
+ 250.7 mb
+
+
+
+ +15 ft
+
+
+
+
+
+ Arroyo Grande
+
+
+
+ California
+
+
+
+
+ United States
+
+
+ )}
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/hotspots/[address]/page.tsx b/src/app/new/hex/[hex]/hotspots/[address]/page.tsx
new file mode 100644
index 0000000..902aa1e
--- /dev/null
+++ b/src/app/new/hex/[hex]/hotspots/[address]/page.tsx
@@ -0,0 +1,33 @@
+import { InfoWrapper } from "@/components/shared/InfoWrapper"
+import clsx from "clsx"
+import { ConnectedDevices } from "./ConnectedDevices"
+import { DetailsHeaderProps } from "./DetailsHeader"
+import { ExplorerOptions } from "./ExplorerOptions"
+import { Insights } from "./Insights"
+import { TechnicalInfo } from "./TechnicalInfo"
+import { OpenCardProvider } from "./useOpenCard"
+
+export const metadata = {
+ title: "Helium Hotspots Map - Hotspot Details",
+}
+
+type Params = {
+ hex: string
+ address: string
+}
+
+export default function Page({ params }: { params: Params }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/new/hex/[hex]/hotspots/[address]/useOpenCard.tsx b/src/app/new/hex/[hex]/hotspots/[address]/useOpenCard.tsx
new file mode 100644
index 0000000..dbaa1b7
--- /dev/null
+++ b/src/app/new/hex/[hex]/hotspots/[address]/useOpenCard.tsx
@@ -0,0 +1,44 @@
+"use client"
+
+import {
+ PropsWithChildren,
+ createContext,
+ useCallback,
+ useContext,
+ useState,
+} from "react"
+
+type OpenCardContext = {
+ openCard: string
+ setOpenCard: (openCard: string) => void
+}
+
+const OpenCardContext = createContext({
+ openCard: "",
+ setOpenCard: () => undefined,
+})
+
+export const OpenCardProvider = ({ children }: PropsWithChildren) => {
+ const [openCard, setOpenCard] = useState("")
+ const setOpenCardCb = useCallback((openCardLabel: string) => {
+ setOpenCard((currentOpenCard) => {
+ if (currentOpenCard === openCardLabel) return ""
+ return openCardLabel
+ })
+ }, [])
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useOpenCard = () => {
+ return useContext(OpenCardContext)
+}
diff --git a/src/app/new/hex/[hex]/page.tsx b/src/app/new/hex/[hex]/page.tsx
new file mode 100644
index 0000000..571e33e
--- /dev/null
+++ b/src/app/new/hex/[hex]/page.tsx
@@ -0,0 +1,18 @@
+import { InfoWrapper } from "@/components/shared/InfoWrapper"
+import { HexPage } from "./HexPage"
+
+export const metadata = {
+ title: "Helium Hotspots Map - Hotspot Details",
+}
+
+type Params = {
+ hex: string
+}
+
+export default function Page({ params }: { params: Params }) {
+ return (
+
+
+
+ )
+}
diff --git a/src/app/new/page.tsx b/src/app/new/page.tsx
new file mode 100644
index 0000000..a56e2a5
--- /dev/null
+++ b/src/app/new/page.tsx
@@ -0,0 +1,7 @@
+export const metadata = {
+ title: "Helium Hotspots Map",
+}
+
+export default function Page() {
+ return null
+}
diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx
index 139bb96..99f60d0 100644
--- a/src/components/Header/index.tsx
+++ b/src/components/Header/index.tsx
@@ -27,6 +27,7 @@ function Logo({ className, ...props }: { className?: string }) {
}
export function Header() {
+ if (true) return null
return (
diff --git a/src/components/icons/Coverage.tsx b/src/components/icons/Coverage.tsx
new file mode 100644
index 0000000..50db743
--- /dev/null
+++ b/src/components/icons/Coverage.tsx
@@ -0,0 +1,17 @@
+export const Coverage = () => {
+ return (
+
+ )
+}
diff --git a/src/components/icons/EnergyIcon.tsx b/src/components/icons/EnergyIcon.tsx
new file mode 100644
index 0000000..e19316a
--- /dev/null
+++ b/src/components/icons/EnergyIcon.tsx
@@ -0,0 +1,17 @@
+export const EnergyIcon = (props: { className?: string }) => {
+ return (
+
+ )
+}
diff --git a/src/components/icons/HeliumIcon2.tsx b/src/components/icons/HeliumIcon2.tsx
new file mode 100644
index 0000000..6a9e7b6
--- /dev/null
+++ b/src/components/icons/HeliumIcon2.tsx
@@ -0,0 +1,19 @@
+export const HeliumIcon2 = (props: { className?: string }) => {
+ return (
+
+ )
+}
diff --git a/src/components/icons/HeliumIotIcon.tsx b/src/components/icons/HeliumIotIcon.tsx
index 3786afa..b965846 100644
--- a/src/components/icons/HeliumIotIcon.tsx
+++ b/src/components/icons/HeliumIotIcon.tsx
@@ -1,6 +1,7 @@
import { HELIUM_IOT_COLOR } from "../HotspotsMap/utils"
-export function HeliumIotIcon(props: { className?: string }) {
+export function HeliumIotIcon(props: { className?: string; fill?: string }) {
+ const fill = props.fill || HELIUM_IOT_COLOR
return (
)
diff --git a/src/components/icons/HexOutlineIcon.tsx b/src/components/icons/HexOutlineIcon.tsx
new file mode 100644
index 0000000..785e169
--- /dev/null
+++ b/src/components/icons/HexOutlineIcon.tsx
@@ -0,0 +1,15 @@
+export const HexOutlineIcon = (props: { stroke?: string }) => (
+
+)
diff --git a/src/components/icons/IotIcon.tsx b/src/components/icons/IotIcon.tsx
new file mode 100644
index 0000000..34e4726
--- /dev/null
+++ b/src/components/icons/IotIcon.tsx
@@ -0,0 +1,22 @@
+import { HELIUM_IOT_COLOR } from "../HotspotsMap/utils"
+
+export function IotIcon(props: { className?: string; fill?: string }) {
+ const fill = props.fill || HELIUM_IOT_COLOR
+ return (
+
+ )
+}
diff --git a/src/components/icons/MobileIcon.tsx b/src/components/icons/MobileIcon.tsx
new file mode 100644
index 0000000..e75739a
--- /dev/null
+++ b/src/components/icons/MobileIcon.tsx
@@ -0,0 +1,25 @@
+export const MobileIcon = (props: { className?: string; fill?: string }) => (
+
+)
diff --git a/src/components/shared/InfoCard.tsx b/src/components/shared/InfoCard.tsx
new file mode 100644
index 0000000..a3feaed
--- /dev/null
+++ b/src/components/shared/InfoCard.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+import { useOpenCard } from "@/app/new/hex/[hex]/hotspots/[address]/useOpenCard"
+import clsx from "clsx"
+import { PropsWithChildren } from "react"
+
+type InfoCardProps = {
+ reducedPadding?: boolean
+ isLast?: boolean
+ isFirst?: boolean
+ label?: string
+}
+
+export const InfoCard = ({
+ children,
+ reducedPadding,
+ isLast = false,
+ isFirst = false,
+ label,
+}: PropsWithChildren) => {
+ const { openCard } = useOpenCard()
+ const isActive = openCard === label
+ const hide = !!openCard && label && !isActive
+
+ let padding = "gap-5 p-[20px]"
+ if (isFirst) padding += " pt-0 sm:pt-[20px]"
+ if (reducedPadding) padding = "p-1"
+
+ return (
+
+
+ {children}
+
+ {!isLast && !openCard && (
+
+ )}
+
+ )
+}
+
+// to be used in tandem with reducedPadding
+export const InfoCardBody = ({ children }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/shared/InfoWrapper.module.css b/src/components/shared/InfoWrapper.module.css
new file mode 100644
index 0000000..eade1ab
--- /dev/null
+++ b/src/components/shared/InfoWrapper.module.css
@@ -0,0 +1,22 @@
+@media (max-width: 640px) {
+ .wrapper {
+ backdrop-filter: blur(8px);
+ max-height: calc(100vh * 2 / 3);
+ }
+}
+
+@media (min-width: 640px) {
+ .wrapper {
+ max-height: calc(100vh - 24 * 4px - 24px);
+ }
+}
+
+.wrapper {
+ scrollbar-gutter: stable;
+ scrollbar-color: #404040b3 transparent; /*firefox*/
+ z-index: 1;
+}
+.wrapper::-webkit-scrollbar {
+ border: none;
+ background-color: transparent;
+}
diff --git a/src/components/shared/InfoWrapper.tsx b/src/components/shared/InfoWrapper.tsx
new file mode 100644
index 0000000..7c89dc0
--- /dev/null
+++ b/src/components/shared/InfoWrapper.tsx
@@ -0,0 +1,65 @@
+"use client"
+
+import clsx from "clsx"
+import { PropsWithChildren, createContext, useContext, useState } from "react"
+import styles from "./InfoWrapper.module.css"
+
+type InfoWrapperContext = {
+ isOpen: boolean
+ setIsOpen: (isOpen: boolean) => void
+}
+
+const InfoWrapperContext = createContext({
+ isOpen: false,
+ setIsOpen: () => undefined,
+})
+
+const InfoWrapperProvider = ({ children }: PropsWithChildren) => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ return (
+
+ {children}
+
+ )
+}
+
+export const useInfoWrapper = () => {
+ return useContext(InfoWrapperContext)
+}
+
+export const InfoWrapperComponent = ({ children }: PropsWithChildren) => {
+ const { isOpen, setIsOpen } = useInfoWrapper()
+
+ return (
+
+
setIsOpen(!isOpen)}
+ >
+
+
+ {children}
+
+ )
+}
+
+export const InfoWrapper = ({ children }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/src/components/shared/Overlay.tsx b/src/components/shared/Overlay.tsx
new file mode 100644
index 0000000..f870f04
--- /dev/null
+++ b/src/components/shared/Overlay.tsx
@@ -0,0 +1,67 @@
+import { Dialog, Transition } from "@headlessui/react"
+import clsx from "clsx"
+import { Fragment, PropsWithChildren, useCallback } from "react"
+
+type OverlayProps = {
+ isOpen: boolean
+ setIsOpen: (bool: boolean) => void
+}
+
+export const Overlay = ({
+ children,
+ isOpen,
+ setIsOpen,
+}: PropsWithChildren) => {
+ const closeOverlay = useCallback(() => {
+ setIsOpen(false)
+ }, [setIsOpen])
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/shared/RadioCircles.tsx b/src/components/shared/RadioCircles.tsx
new file mode 100644
index 0000000..5a4f7dc
--- /dev/null
+++ b/src/components/shared/RadioCircles.tsx
@@ -0,0 +1,16 @@
+import clsx from "clsx"
+
+type RadioCirclesProps = {
+ isActive: boolean
+}
+
+export const RadioCircles = ({ isActive }: RadioCirclesProps) => (
+
+)
diff --git a/src/components/shared/RssiPill.tsx b/src/components/shared/RssiPill.tsx
new file mode 100644
index 0000000..087cd32
--- /dev/null
+++ b/src/components/shared/RssiPill.tsx
@@ -0,0 +1,39 @@
+import clsx from "clsx"
+
+export type RssiStrength = "strong" | "medium" | "low" | "coverage" | number
+
+export type RssiPillProps = {
+ strength: RssiStrength
+ isCircle?: boolean
+ isEmpty?: boolean
+}
+
+export const getRssiColor = (strength: RssiStrength) => {
+ if (typeof strength === "number") {
+ if (strength >= 90) strength = "strong"
+ else if (strength >= 70) strength = "medium"
+ else strength = "low"
+ }
+
+ if (strength === "strong") return "bg-[#FF4D00]"
+ if (strength === "medium") return "bg-[#FFD600]"
+ if (strength === "low") return "bg-[#01FFF0]"
+ if (strength === "coverage")
+ return "bg-gradient-to-r from-[#01FFF0] via-[#FFD600] to-[#FF4D00]"
+}
+
+export const RssiPill = ({
+ strength,
+ isCircle = false,
+ isEmpty = false,
+}: RssiPillProps) => {
+ return (
+
+ )
+}
diff --git a/src/context/usePreferences.tsx b/src/context/usePreferences.tsx
index c894f2e..38f547b 100644
--- a/src/context/usePreferences.tsx
+++ b/src/context/usePreferences.tsx
@@ -1,5 +1,10 @@
"use client"
-import { PROVIDERS, Provider } from "@/app/preferences/components/ProviderList"
+
+import { NO_PREFERENCE } from "@/app/new/Settings/HotspotProviders"
+import { Provider } from "@/app/preferences/components/ProviderList"
+import { HotspottyIcon } from "@/components/icons/HotspottyIcon"
+import { MokenIcon } from "@/components/icons/MokenIcon"
+import { RelayIcon } from "@/components/icons/RelayIcon"
import {
PropsWithChildren,
createContext,
@@ -9,23 +14,65 @@ import {
useState,
} from "react"
+const shuffle = (arr: T[]) => {
+ let i = arr.length,
+ j,
+ temp
+ while (--i > 0) {
+ j = Math.floor(Math.random() * (i + 1))
+ temp = arr[j]
+ arr[j] = arr[i]
+ arr[i] = temp
+ }
+ return arr
+}
+
+export const PROVIDERS: Provider[] = shuffle([
+ {
+ Icon: ,
+ label: "Hotspotty",
+ getUrl: (hotspotId: string) =>
+ `https://app.hotspotty.net/hotspots/${hotspotId}/rewards`,
+ },
+ {
+ Icon: ,
+ label: "Moken",
+ getUrl: (hotspotId: string) =>
+ `https://explorer.moken.io/hotspots/${hotspotId}`,
+ },
+ {
+ Icon: ,
+ label: "Relay",
+ getUrl: (hotspotId: string) =>
+ `https://explorer.relaywireless.com/hotspots/${hotspotId}`,
+ },
+])
+
type PreferencesContext = {
provider?: Provider
setProvider: (provider: Provider) => void
+ savePreference: boolean
+ setSavePreference: (preference: boolean) => void
}
const PreferencesContext = createContext({
provider: undefined,
setProvider: () => undefined,
+ savePreference: false,
+ setSavePreference: () => undefined,
})
const PROVIDER_KEY = "provider"
+const SAVE_PREFERENCE = "save_preference"
const VERSION_KEY = "version"
// change version to reset provider preference
const VERSION = "3"
const getProvider = (providerLabel?: string) => {
- return PROVIDERS.find((provider) => provider.label === providerLabel)
+ return (
+ PROVIDERS.find((provider) => provider.label === providerLabel) ||
+ NO_PREFERENCE
+ )
}
const getLocalValue = (key: string) => {
@@ -43,7 +90,18 @@ export const PreferencesProvider = ({ children }: PropsWithChildren) => {
const [provider, setProvider] = useState(
localVersion === VERSION
? getProvider(getLocalValue(PROVIDER_KEY) || "")
- : undefined
+ : NO_PREFERENCE
+ )
+ const [savePreference, setSavePreference] = useState(
+ getLocalValue(SAVE_PREFERENCE) === "true"
+ )
+ const setSavePreferenceCB = useCallback(
+ (preference: boolean) => {
+ if (!!window)
+ localStorage?.setItem(SAVE_PREFERENCE, preference.toString())
+ setSavePreference(preference)
+ },
+ [setSavePreference]
)
const setProviderCB = useCallback(
@@ -60,6 +118,8 @@ export const PreferencesProvider = ({ children }: PropsWithChildren) => {
value={{
provider,
setProvider: setProviderCB,
+ savePreference,
+ setSavePreference: setSavePreferenceCB,
}}
>
{children}
diff --git a/src/knex/supplyLimit.ts b/src/knex/supplyLimit.ts
index a6bf416..ced3432 100644
--- a/src/knex/supplyLimit.ts
+++ b/src/knex/supplyLimit.ts
@@ -56,6 +56,9 @@ export class SupplyLimit {
const hasBurnIncrease =
record.hnt_burned > (latestBurn?.hnt_burned || BigInt(0))
+ // HEROKU_PR_NUMBER injected to env vars when review app.
+ if (!!process.env.HEROKU_PR_NUMBER) return latest || latestBurn || record
+
if (
// first time app is run
(!latest && !latestBurn) ||
diff --git a/src/styles/tailwind.css b/src/styles/tailwind.css
index 76fcadc..4bc5f42 100644
--- a/src/styles/tailwind.css
+++ b/src/styles/tailwind.css
@@ -1,3 +1,7 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
+
+button {
+ text-rendering: geometricPrecision;
+}
diff --git a/tailwind.config.js b/tailwind.config.js
index 1cf5f5f..ae116c4 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -18,7 +18,11 @@ module.exports = {
"8xl": ["6rem", { lineHeight: "1" }],
"9xl": ["8rem", { lineHeight: "1" }],
},
- extend: {},
+ extend: {
+ fontFamily: {
+ sans: ["var(--font-dm-sans)"],
+ },
+ },
},
plugins: [
require("@tailwindcss/typography"),
diff --git a/tsconfig.json b/tsconfig.json
index d0eb9ab..b1ec685 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -21,7 +21,8 @@
}
],
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["./src/*"],
+ "@public/*": ["./public/*"]
},
"strictNullChecks": true
},