Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
79 lines
2.1 KiB
TypeScript
79 lines
2.1 KiB
TypeScript
'use client'
|
|
|
|
import { useMemo } from 'react'
|
|
import { MapContainer, TileLayer, CircleMarker, Tooltip } from 'react-leaflet'
|
|
import 'leaflet/dist/leaflet.css'
|
|
import { COUNTRIES } from '@/lib/countries'
|
|
|
|
|
|
type CountryData = { countryCode: string; count: number }
|
|
|
|
type LeafletMapProps = {
|
|
data: CountryData[]
|
|
compact?: boolean
|
|
}
|
|
|
|
export default function LeafletMap({ data, compact }: LeafletMapProps) {
|
|
const markers = useMemo(() => {
|
|
const maxCount = Math.max(...data.map((d) => d.count), 1)
|
|
return data
|
|
.filter((d) => d.countryCode !== 'UNKNOWN' && COUNTRIES[d.countryCode])
|
|
.map((d) => {
|
|
const country = COUNTRIES[d.countryCode]
|
|
const ratio = d.count / maxCount
|
|
const radius = 5 + ratio * 15
|
|
return {
|
|
code: d.countryCode,
|
|
name: country.name,
|
|
position: [country.lat, country.lng] as [number, number],
|
|
count: d.count,
|
|
radius,
|
|
ratio,
|
|
}
|
|
})
|
|
}, [data])
|
|
|
|
return (
|
|
<MapContainer
|
|
center={[20, 0]}
|
|
zoom={compact ? 1 : 2}
|
|
scrollWheelZoom
|
|
zoomControl
|
|
dragging
|
|
doubleClickZoom
|
|
style={{
|
|
height: compact ? 400 : 500,
|
|
width: '100%',
|
|
borderRadius: '0.5rem',
|
|
background: '#f0f0f0',
|
|
}}
|
|
attributionControl={false}
|
|
>
|
|
<TileLayer
|
|
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png"
|
|
/>
|
|
{markers.map((marker) => (
|
|
<CircleMarker
|
|
key={marker.code}
|
|
center={marker.position}
|
|
radius={marker.radius}
|
|
pathOptions={{
|
|
color: '#de0f1e',
|
|
fillColor: '#de0f1e',
|
|
fillOpacity: 0.35 + marker.ratio * 0.45,
|
|
weight: 1.5,
|
|
}}
|
|
>
|
|
<Tooltip direction="top" offset={[0, -marker.radius]}>
|
|
<div className="text-xs font-medium">
|
|
<span className="font-semibold">{marker.name}</span>
|
|
<br />
|
|
{marker.count} project{marker.count !== 1 ? 's' : ''}
|
|
</div>
|
|
</Tooltip>
|
|
</CircleMarker>
|
|
))}
|
|
</MapContainer>
|
|
)
|
|
}
|