Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions components/search/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ export const actions: Action[] = [
subtitle: 'Rotating Globe with grab',
perform: () => (window.location.href = '/globe'),
},
{
id: 'map',
name: '3D Map',
section: 'Awesome',
icon: <Code2 size={18} />,
subtitle: 'Full screen 3D map centered on your location',
perform: () => (window.location.href = '/map'),
},
{
id: 'emoji',
name: 'Emoji',
Expand Down
37 changes: 30 additions & 7 deletions pages/api/ip-lookup.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,49 @@
import type { NextApiRequest, NextApiResponse } from 'next'

type Data = {
latitude: number,
latitude: number
longitude: number
}

type ApiResponse = {
loc: string
loc?: string
}

const fallbackLocation: Data = {
latitude: 40.7128,
longitude: -74.006,
}

function getClientIp(req: NextApiRequest) {
const forwardedFor = req.headers['x-forwarded-for']
const forwardedIp = Array.isArray(forwardedFor)
? forwardedFor[0]
: forwardedFor?.split(',')[0]
const cloudflareIp = req.headers['cf-connecting-ip']
const headerIp = Array.isArray(cloudflareIp) ? cloudflareIp[0] : cloudflareIp

return headerIp || forwardedIp || req.socket.remoteAddress || ''
}

export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
res: NextApiResponse<Data>,
) {
const clientIP = req.headers['cf-connecting-ip']
const ipResponse = await fetch(`https://ipinfo.io/${clientIP}/json?token=ef8461623ce04a`)
const clientIp = getClientIp(req)
const ipResponse = await fetch(
`https://ipinfo.io/${clientIp}/json?token=ef8461623ce04a`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛑 Security Vulnerability: Hardcoded API token exposes credentials in source code. Move the ipinfo.io API token to an environment variable to prevent unauthorized access and token theft.1

Suggested change
`https://ipinfo.io/${clientIp}/json?token=ef8461623ce04a`,
`

Footnotes

  1. CWE-798: Use of Hard-coded Credentials - https://cwe.mitre.org/data/definitions/798.html

)
Comment on lines +33 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The IPInfo API token is hardcoded in the fetch URL. Hardcoding API keys or tokens in source code is a security risk and makes it difficult to rotate keys. Please move this token to an environment variable (e.g., process.env.IPINFO_TOKEN) and access it securely.

Suggested change
const ipResponse = await fetch(
`https://ipinfo.io/${clientIp}/json?token=ef8461623ce04a`,
)
const token = process.env.IPINFO_TOKEN || 'ef8461623ce04a'
const ipResponse = await fetch(
`https://ipinfo.io/${clientIp}/json?token=${token}`,
)

const apiResponse: ApiResponse = await ipResponse.json()
Comment on lines +33 to 36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add error handling for fetch failures. Network errors or API downtime will crash the handler without proper error handling.1

Suggested change
const ipResponse = await fetch(
`https://ipinfo.io/${clientIp}/json?token=ef8461623ce04a`,
)
const apiResponse: ApiResponse = await ipResponse.json()
try {
const ipResponse = await fetch(
`
)
if (!ipResponse.ok) {
res.status(200).json(fallbackLocation)
return
}
const apiResponse: ApiResponse = await ipResponse.json()

Footnotes

  1. CWE-755: Improper Handling of Exceptional Conditions - https://cwe.mitre.org/data/definitions/755.html


const [lat, long] = apiResponse.loc.split(",")
if (!apiResponse.loc) {
res.status(200).json(fallbackLocation)
return
}

const [lat, long] = apiResponse.loc.split(',')
const data: Data = {
latitude: parseFloat(lat),
longitude: parseFloat(long)
longitude: parseFloat(long),
}

res.status(200).json(data)
Comment on lines 28 to 49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add error handling for JSON parsing and API response errors. Without a try-catch block, fetch failures, network errors, and JSON parsing errors will cause unhandled promise rejections and crash the API handler.1

Suggested change
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>
res: NextApiResponse<Data>,
) {
const clientIP = req.headers['cf-connecting-ip']
const ipResponse = await fetch(`https://ipinfo.io/${clientIP}/json?token=ef8461623ce04a`)
const clientIp = getClientIp(req)
const ipResponse = await fetch(
`https://ipinfo.io/${clientIp}/json?token=ef8461623ce04a`,
)
const apiResponse: ApiResponse = await ipResponse.json()
const [lat, long] = apiResponse.loc.split(",")
if (!apiResponse.loc) {
res.status(200).json(fallbackLocation)
return
}
const [lat, long] = apiResponse.loc.split(',')
const data: Data = {
latitude: parseFloat(lat),
longitude: parseFloat(long)
longitude: parseFloat(long),
}
res.status(200).json(data)
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<Data>,
) {
try {
const clientIp = getClientIp(req)
const ipResponse = await fetch(
`
)
if (!ipResponse.ok) {
res.status(200).json(fallbackLocation)
return
}
const apiResponse: ApiResponse = await ipResponse.json()
if (!apiResponse.loc) {
res.status(200).json(fallbackLocation)
return
}
const [lat, long] = apiResponse.loc.split(',')
const data: Data = {
latitude: parseFloat(lat),
longitude: parseFloat(long),
}
res.status(200).json(data)
} catch (error) {
res.status(200).json(fallbackLocation)
}
}

Footnotes

  1. CWE-755: Improper Handling of Exceptional Conditions - https://cwe.mitre.org/data/definitions/755.html

Expand Down
281 changes: 281 additions & 0 deletions pages/map.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,281 @@
import Head from 'next/head'
import Script from 'next/script'
import { useEffect, useRef, useState } from 'react'
import { MapPin } from 'lucide-react'

import Page from '@/components/page'

type Coordinates = {
latitude: number
longitude: number
}

type LocationSource = 'browser' | 'ip' | 'default'

type LocationState = {
coordinates: Coordinates
source: LocationSource
label: string
}

type MapLibre = {
Map: new (options: Record<string, unknown>) => MapInstance
Marker: new (options?: Record<string, unknown>) => MapMarker
NavigationControl: new (options?: Record<string, unknown>) => unknown
GeolocateControl: new (options?: Record<string, unknown>) => unknown
FullscreenControl: new (options?: Record<string, unknown>) => unknown
}

type MapInstance = {
addControl: (control: unknown, position?: string) => void
flyTo: (options: Record<string, unknown>) => void
remove: () => void
}

type MapMarker = {
setLngLat: (coordinates: [number, number]) => MapMarker
addTo: (map: MapInstance) => MapMarker
remove: () => void
}

declare global {
interface Window {
maplibregl?: MapLibre
}
}

const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty'
const DEFAULT_LOCATION: LocationState = {
coordinates: {
latitude: 40.7128,
longitude: -74.006,
},
source: 'default',
label: 'default location',
}

const browserLocationOptions: PositionOptions = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 60000,
}

function getBrowserLocation() {
return new Promise<LocationState>((resolve, reject) => {
if (!navigator.geolocation) {
reject(new Error('Browser geolocation is not available'))
return
}

navigator.geolocation.getCurrentPosition(
(position) => {
resolve({
coordinates: {
latitude: position.coords.latitude,
longitude: position.coords.longitude,
},
source: 'browser',
label: 'browser location',
})
},
reject,
browserLocationOptions,
)
})
}

async function getIpLocation() {
const response = await fetch('/api/ip-lookup')

if (!response.ok) {
throw new Error('IP location lookup failed')
}

const coordinates: Coordinates = await response.json()

if (
!Number.isFinite(coordinates.latitude) ||
!Number.isFinite(coordinates.longitude)
) {
throw new Error('IP location lookup returned invalid coordinates')
}

return {
coordinates,
source: 'ip' as const,
label: 'IP address location',
}
}

const FullScreen3DMap = () => {
const mapContainerRef = useRef<HTMLDivElement | null>(null)
const mapRef = useRef<MapInstance | null>(null)
const markerRef = useRef<MapMarker | null>(null)
const [isMapLibreReady, setIsMapLibreReady] = useState(false)
const [location, setLocation] = useState<LocationState | null>(null)
const [status, setStatus] = useState('Requesting browser location…')

useEffect(() => {
let cancelled = false

const loadLocation = async () => {
try {
const browserLocation = await getBrowserLocation()

if (!cancelled) {
setLocation(browserLocation)
setStatus('Centered on your browser location')
}
} catch {
if (!cancelled) {
setStatus('Browser location unavailable. Falling back to IP address…')
}

try {
const ipLocation = await getIpLocation()

if (!cancelled) {
setLocation(ipLocation)
setStatus('Centered on your IP address location')
}
} catch {
if (!cancelled) {
setLocation(DEFAULT_LOCATION)
setStatus('Location lookup failed. Showing the default map center')
}
}
}
}

loadLocation()

return () => {
cancelled = true
}
}, [])

useEffect(() => {
if (
!isMapLibreReady ||
!location ||
!mapContainerRef.current ||
mapRef.current
) {
return
}

const maplibregl = window.maplibregl

if (!maplibregl) {
setStatus('Map library failed to load')
return
}

const center: [number, number] = [
location.coordinates.longitude,
location.coordinates.latitude,
]
const map = new maplibregl.Map({
container: mapContainerRef.current,
style: MAP_STYLE,
center,
zoom: 16,
pitch: 60,
bearing: -20,
antialias: true,
attributionControl: { compact: true },
})

map.addControl(
new maplibregl.NavigationControl({ visualizePitch: true }),
'top-right',
)
map.addControl(
new maplibregl.GeolocateControl({
positionOptions: browserLocationOptions,
trackUserLocation: true,
showUserHeading: true,
}),
'top-right',
)
map.addControl(new maplibregl.FullscreenControl(), 'top-right')

markerRef.current = new maplibregl.Marker({ color: '#38bdf8' })
.setLngLat(center)
.addTo(map)
mapRef.current = map

return () => {
markerRef.current?.remove()
map.remove()
markerRef.current = null
mapRef.current = null
}
}, [isMapLibreReady, location])
Comment on lines +157 to +214

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The useEffect hook that initializes the map has location in its dependency array. In React, even if you return early using mapRef.current, the cleanup function from the previous execution of the effect is run before the next execution. This means that whenever location changes, the map is destroyed (map.remove()) and recreated from scratch, causing a poor user experience and unnecessary API/tile requests.

To fix this, you should separate the map initialization from location updates. You can track whether the location has been loaded using a boolean flag (e.g., hasLocation = location !== null) and use that as a dependency instead of the location object itself. This ensures the map is initialized only once when the location first becomes available, and subsequent location updates are handled smoothly by the other useEffect hook.

	const hasLocation = location !== null
	const locationRef = useRef(location)
	useEffect(() => {
		locationRef.current = location
	}, [location])

	useEffect(() => {
		if (
			!isMapLibreReady ||
			!hasLocation ||
			!mapContainerRef.current ||
			mapRef.current
		) {
			return
		}

		const maplibregl = window.maplibregl

		if (!maplibregl || !locationRef.current) {
			setStatus('Map library failed to load')
			return
		}

		const center: [number, number] = [
			locationRef.current.coordinates.longitude,
			locationRef.current.coordinates.latitude,
		]
		const map = new maplibregl.Map({
			container: mapContainerRef.current,
			style: MAP_STYLE,
			center,
			zoom: 16,
			pitch: 60,
			bearing: -20,
			antialias: true,
			attributionControl: { compact: true },
		})

		map.addControl(
			new maplibregl.NavigationControl({ visualizePitch: true }),
			'top-right',
		)
		map.addControl(
			new maplibregl.GeolocateControl({
				positionOptions: browserLocationOptions,
				trackUserLocation: true,
				showUserHeading: true,
			}),
			'top-right',
		)
		map.addControl(new maplibregl.FullscreenControl(), 'top-right')

		markerRef.current = new maplibregl.Marker({ color: '#38bdf8' })
			.setLngLat(center)
			.addTo(map)
		mapRef.current = map

		return () => {
			markerRef.current?.remove()
			map.remove()
			markerRef.current = null
			mapRef.current = null
		}
	}, [isMapLibreReady, hasLocation])


useEffect(() => {
if (!location || !mapRef.current || !markerRef.current) {
return
}

const center: [number, number] = [
location.coordinates.longitude,
location.coordinates.latitude,
]

markerRef.current.setLngLat(center)
mapRef.current.flyTo({
center,
zoom: 16,
pitch: 60,
bearing: -20,
duration: 1200,
})
}, [location])

return (
<>
<Head>
<title>3D Map Wake Lock</title>
<link
rel='stylesheet'
href='https://unpkg.com/maplibre-gl@5.6.2/dist/maplibre-gl.css'
/>
</Head>
<Script
src='https://unpkg.com/maplibre-gl@5.6.2/dist/maplibre-gl.js'
strategy='afterInteractive'
onLoad={() => setIsMapLibreReady(true)}
onError={() => setStatus('Map library failed to load')}
/>

<div className='fixed inset-0 cursor-auto bg-slate-950 text-white'>
<div ref={mapContainerRef} className='h-screen w-screen' />

<div className='pointer-events-none fixed left-4 top-4 z-10 max-w-sm rounded-2xl border border-white/15 bg-slate-950/75 p-4 shadow-2xl backdrop-blur'>
<div className='flex items-center gap-2 text-sm font-semibold uppercase tracking-[0.25em] text-sky-300'>
<MapPin size={16} />
3D Map
</div>
<p className='mt-2 text-sm text-slate-100'>{status}</p>
{location && (
<p className='mt-2 font-mono text-xs text-slate-300'>
{location.coordinates.latitude.toFixed(5)},{' '}
{location.coordinates.longitude.toFixed(5)} · {location.label}
</p>
)}
</div>
</div>
</>
)
}

const MapPage = () => {
return (
<Page>
<FullScreen3DMap />
</Page>
)
}

export default MapPage