diff options
Diffstat (limited to 'src/components/ui/world-map.tsx')
-rw-r--r-- | src/components/ui/world-map.tsx | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/src/components/ui/world-map.tsx b/src/components/ui/world-map.tsx new file mode 100644 index 0000000..d6c9891 --- /dev/null +++ b/src/components/ui/world-map.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useRef } from "react"; +import { motion } from "motion/react"; +import DottedMap from "dotted-map"; + +import { useTheme } from "next-themes"; + +interface MapProps { + dots?: Array<{ + start: { lat: number; lng: number; label?: string }; + end: { lat: number; lng: number; label?: string }; + }>; + lineColor?: string; +} + +export function WorldMap({ dots = [], lineColor = "#0ea5e9" }: MapProps) { + const svgRef = useRef<SVGSVGElement>(null); + const map = new DottedMap({ height: 100, grid: "diagonal" }); + + const { theme } = useTheme(); + + const svgMap = map.getSVG({ + radius: 0.22, + color: theme === "dark" ? "#FFFFFF40" : "#00000040", + shape: "circle", + backgroundColor: theme === "dark" ? "black" : "white", + }); + + const projectPoint = (lat: number, lng: number) => { + const x = (lng + 180) * (800 / 360); + const y = (90 - lat) * (400 / 180); + return { x, y }; + }; + + const createCurvedPath = ( + start: { x: number; y: number }, + end: { x: number; y: number } + ) => { + const midX = (start.x + end.x) / 2; + const midY = Math.min(start.y, end.y) - 50; + return `M ${start.x} ${start.y} Q ${midX} ${midY} ${end.x} ${end.y}`; + }; + + return ( + <div className="w-full aspect-[2/1] dark:bg-black bg-white rounded-lg relative font-sans"> + <img + src={`data:image/svg+xml;utf8,${encodeURIComponent(svgMap)}`} + className="h-full w-full [mask-image:linear-gradient(to_bottom,transparent,white_10%,white_90%,transparent)] pointer-events-none select-none" + alt="world map" + height="495" + width="1056" + draggable={false} + /> + <svg + ref={svgRef} + viewBox="0 0 800 400" + className="w-full h-full absolute inset-0 pointer-events-none select-none" + > + {dots.map((dot, i) => { + const startPoint = projectPoint(dot.start.lat, dot.start.lng); + const endPoint = projectPoint(dot.end.lat, dot.end.lng); + return ( + <g key={`path-group-${i}`}> + <motion.path + d={createCurvedPath(startPoint, endPoint)} + fill="none" + stroke="url(#path-gradient)" + strokeWidth="1" + initial={{ + pathLength: 0, + }} + animate={{ + pathLength: 1, + }} + transition={{ + duration: 0, + delay: 0.5 * i, + ease: "easeOut", + }} + key={`start-upper-${i}`} + ></motion.path> + </g> + ); + })} + + <defs> + <linearGradient id="path-gradient" x1="0%" y1="0%" x2="100%" y2="0%"> + <stop offset="0%" stopColor="white" stopOpacity="0" /> + <stop offset="5%" stopColor={lineColor} stopOpacity="1" /> + <stop offset="95%" stopColor={lineColor} stopOpacity="1" /> + <stop offset="100%" stopColor="white" stopOpacity="0" /> + </linearGradient> + </defs> + + {dots.map((dot, i) => ( + <g key={`points-group-${i}`}> + <g key={`start-${i}`}> + <circle + cx={projectPoint(dot.start.lat, dot.start.lng).x} + cy={projectPoint(dot.start.lat, dot.start.lng).y} + r="2" + fill={lineColor} + /> + <circle + cx={projectPoint(dot.start.lat, dot.start.lng).x} + cy={projectPoint(dot.start.lat, dot.start.lng).y} + r="2" + fill={lineColor} + opacity="0.5" + > + <animate + attributeName="r" + from="2" + to="8" + dur="1.5s" + begin="0s" + repeatCount="indefinite" + /> + <animate + attributeName="opacity" + from="0.5" + to="0" + dur="1.5s" + begin="0s" + repeatCount="indefinite" + /> + </circle> + </g> + <g key={`end-${i}`}> + <circle + cx={projectPoint(dot.end.lat, dot.end.lng).x} + cy={projectPoint(dot.end.lat, dot.end.lng).y} + r="2" + fill={lineColor} + /> + <circle + cx={projectPoint(dot.end.lat, dot.end.lng).x} + cy={projectPoint(dot.end.lat, dot.end.lng).y} + r="2" + fill={lineColor} + opacity="0.5" + > + <animate + attributeName="r" + from="2" + to="8" + dur="1.5s" + begin="0s" + repeatCount="indefinite" + /> + <animate + attributeName="opacity" + from="0.5" + to="0" + dur="1.5s" + begin="0s" + repeatCount="indefinite" + /> + </circle> + </g> + </g> + ))} + </svg> + </div> + ); +} |