feat: add tunnel configuration tools

This commit is contained in:
2025-09-06 18:53:38 +07:00
parent d5ffb87e1a
commit 9701d8766e
7 changed files with 1655 additions and 313 deletions

View File

@ -1,64 +0,0 @@
"use client"
import { useState } from "react"
export default function Card() {
const [copied, setCopied] = useState(false)
const command = "ssh id.tunnl.live -p 2200 -R 443:localhost:8000"
const copyToClipboard = () => {
navigator.clipboard.writeText(command)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="mb-16">
<h2 className="text-2xl font-bold mb-6">Connect with a single command</h2>
<div className="relative">
<div className="bg-gray-900 rounded-lg p-4 border border-gray-800 font-mono text-sm sm:text-base overflow-x-auto">
<pre className="whitespace-pre-wrap break-all sm:break-normal">{command}</pre>
</div>
<button
onClick={copyToClipboard}
className="absolute right-3 top-3 h-8 w-8 flex items-center justify-center rounded-md text-gray-400 hover:text-white hover:bg-gray-800"
aria-label="Copy command"
>
{copied ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-emerald-400"
>
<path d="M20 6 9 17l-5-5" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
)}
</button>
</div>
<p className="mt-4 text-sm text-gray-400">
This command creates a secure tunnel from our server to your localhost:8000
</p>
</div>
)
}

View File

@ -1,11 +1,23 @@
import Card from "./card"
"use client"
import { useState } from "react"
import TunnelConfig, { type TunnelConfig as TunnelConfigType, type Server } from "@/components/tunnel-config"
const defaultConfig: TunnelConfigType = {
type: "http",
serverPort: 443,
localPort: 8000,
}
export default function Home() {
const [selectedServer, setSelectedServer] = useState<Server | null>(null)
const [tunnelConfig, setTunnelConfig] = useState<TunnelConfigType>(defaultConfig)
return (
<div className="flex min-h-screen flex-col bg-gray-950 text-white">
<main className="flex-1 flex flex-col items-center justify-center px-4">
<div className="w-full max-w-3xl mx-auto text-center">
<div className="mb-12">
<main className="flex-1 flex flex-col items-center justify-center px-4 py-8">
<div className="w-full max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl md:text-6xl mb-6">
<span className="text-emerald-400">tunnl</span>.live
</h1>
@ -13,7 +25,15 @@ export default function Home() {
Expose your local services to the internet securely with our fast and reliable SSH tunneling service.
</p>
</div>
<Card />
<TunnelConfig
config={tunnelConfig}
onConfigChange={setTunnelConfig}
selectedServer={selectedServer}
onServerSelect={setSelectedServer}
/>
<div className="max-w-3xl mx-auto">
<div className="grid gap-8 md:grid-cols-3 mb-16">
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800">
<div className="mb-4 inline-block rounded-full bg-emerald-950 p-3">
@ -36,7 +56,7 @@ export default function Home() {
</div>
<h3 className="text-xl font-bold mb-2">Global Network</h3>
<p className="text-gray-400">
We offer low-latency, high-availability servers located in Singapore for optimal performance.
Choose from servers in US, Singapore, and Indonesia for optimal performance.
</p>
</div>
<div className="bg-gray-900 p-6 rounded-lg border border-gray-800">
@ -75,14 +95,14 @@ export default function Home() {
strokeLinejoin="round"
className="text-emerald-400"
>
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1-2-2h6" />
<polyline points="15 3 21 3 21 9" />
<line x1="10" x2="21" y1="14" y2="3" />
</svg>
</div>
<h3 className="text-xl font-bold mb-2">Easy Sharing</h3>
<h3 className="text-xl font-bold mb-2">Flexible Configuration</h3>
<p className="text-gray-400">
Share your local development with clients or teammates without complex setup.
Support for both HTTP/HTTPS and TCP tunneling with custom port configuration.
</p>
</div>
</div>
@ -94,6 +114,7 @@ export default function Home() {
</p>
</div>
</div>
</div>
</main>
<footer className="border-t border-gray-800 py-6 px-4">
<div className="max-w-3xl mx-auto text-center">

View File

@ -0,0 +1,106 @@
"use client"
import Image from "next/image"
import Link from "next/link"
export default function TunnelNotFound() {
const exampleUrl = "example.com"
return (
<div className="flex min-h-screen flex-col bg-gray-950 text-white">
<main className="flex-1 flex flex-col items-center justify-center px-4 py-8">
<div className="w-full max-w-2xl mx-auto text-center">
{/* Mascot */}
<div className="mb-8">
<Image
src="/mascot-confused.png"
alt="Confused tunnel mascot"
width={200}
height={200}
className="mx-auto"
/>
</div>
{/* Error Message */}
<div className="mb-12">
<h1 className="text-4xl font-bold mb-6">Tunnel Not Found</h1>
<p className="text-gray-400 mb-6 text-lg">We couldn't find an active tunnel for:</p>
<div className="bg-gray-900 rounded-lg p-4 border border-gray-800 font-mono text-emerald-400 text-xl max-w-md mx-auto mb-8">
{exampleUrl}
</div>
<p className="text-gray-300 text-lg">This means no SSH tunnel is currently running for this domain.</p>
</div>
{/* Instructions */}
<div className="bg-gray-900 rounded-lg p-8 border border-gray-800 mb-8 text-left">
<h2 className="text-xl font-bold mb-6 text-center">To create a tunnel:</h2>
<div className="space-y-4">
<div className="flex items-start gap-4">
<span className="bg-emerald-600 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm font-bold flex-shrink-0 mt-1">
1
</span>
<div>
<p className="text-gray-300 font-medium">Go to the main page</p>
<p className="text-gray-400 text-sm">Configure your tunnel settings and choose a server</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="bg-emerald-600 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm font-bold flex-shrink-0 mt-1">
2
</span>
<div>
<p className="text-gray-300 font-medium">Make sure your local service is running</p>
<p className="text-gray-400 text-sm">Your application should be accessible on localhost</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="bg-emerald-600 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm font-bold flex-shrink-0 mt-1">
3
</span>
<div>
<p className="text-gray-300 font-medium">Run the SSH command</p>
<p className="text-gray-400 text-sm">Copy and paste the generated command into your terminal</p>
</div>
</div>
<div className="flex items-start gap-4">
<span className="bg-emerald-600 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm font-bold flex-shrink-0 mt-1">
4
</span>
<div>
<p className="text-gray-300 font-medium">Access your tunnel</p>
<p className="text-gray-400 text-sm">Your service will be available through the tunnel URL</p>
</div>
</div>
</div>
</div>
{/* Call to Action */}
<div className="space-y-4">
<Link
href="/"
className="inline-flex items-center gap-3 bg-emerald-600 hover:bg-emerald-700 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M5 12h14" />
<path d="M12 5l7 7-7 7" />
</svg>
Create Your Tunnel
</Link>
<p className="text-gray-400 text-sm">Need help? Check our documentation or contact support.</p>
</div>
</div>
</main>
</div>
)
}

View File

@ -0,0 +1,771 @@
"use client"
import { useState, useEffect } from "react"
import { ComposableMap, Geographies, Geography, Marker } from "react-simple-maps"
export interface TunnelConfig {
type: "http" | "tcp"
serverPort: number
localPort: number
}
interface Server {
id: string
name: string
location: string
subdomain: string
coordinates: [number, number]
ping: number | null
status: "online" | "offline" | "maintenance"
pingStatus: "idle" | "testing" | "success" | "failed" | "timeout"
}
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
interface TunnelConfigProps {
config: TunnelConfig
onConfigChange: (config: TunnelConfig) => void
selectedServer: Server | null
onServerSelect: (server: Server) => void
}
const fetchServers = async (): Promise<Server[]> => {
await new Promise((resolve) => setTimeout(resolve, 2000))
const mockServers: Server[] = [
{
id: "sgp",
name: "Singapore",
location: "Singapore",
subdomain: "sgp.tunnl.live",
coordinates: [103.8198, 1.3521],
ping: null,
status: "online",
pingStatus: "idle",
},
{
id: "id",
name: "Indonesia",
location: "Jakarta",
subdomain: "id.tunnl.live",
coordinates: [106.8456, -6.2088],
ping: null,
status: "online",
pingStatus: "idle",
},
]
return mockServers.filter((server) => server.status === "online")
}
const testServerPing = (
server: Server,
): Promise<{ server: Server; ping: number | null; status: Server["pingStatus"] }> => {
return new Promise((resolve) => {
const startTime = Date.now()
const timeout = 5000
let resolved = false
const pingUrl = `wss://ping.${server.subdomain}`
try {
const ws = new WebSocket(pingUrl)
const timeoutId = setTimeout(() => {
if (!resolved) {
resolved = true
ws.close()
resolve({
server,
ping: null,
status: "timeout",
})
}
}, timeout)
ws.onopen = () => {
console.log(`Connected to ${pingUrl}`)
}
ws.onmessage = (event) => {
if (event.data === "pong" && !resolved) {
resolved = true
const ping = Date.now() - startTime
clearTimeout(timeoutId)
ws.close()
resolve({
server,
ping,
status: "success",
})
}
}
ws.onclose = (event) => {
if (!resolved) {
resolved = true
clearTimeout(timeoutId)
resolve({
server,
ping: null,
status: "failed",
})
}
}
ws.onerror = (error) => {
if (!resolved) {
resolved = true
clearTimeout(timeoutId)
console.error(`WebSocket error for ${pingUrl}:`, error)
resolve({
server,
ping: null,
status: "failed",
})
}
}
} catch (error) {
console.error(`Failed to create WebSocket for ${pingUrl}:`, error)
resolve({
server,
ping: null,
status: "failed",
})
}
})
}
export default function TunnelConfig({ config, onConfigChange, selectedServer, onServerSelect }: TunnelConfigProps) {
const [localConfig, setLocalConfig] = useState<TunnelConfig>(config)
const [servers, setServers] = useState<Server[]>([])
const [isLoadingServers, setIsLoadingServers] = useState(true)
const [isTestingPings, setIsTestingPings] = useState(false)
const [hasAutoTested, setHasAutoTested] = useState(false)
const [copied, setCopied] = useState(false)
const [serverError, setServerError] = useState<string | null>(null)
useEffect(() => {
const loadServers = async () => {
try {
setIsLoadingServers(true)
setServerError(null)
const serverData = await fetchServers()
setServers(serverData)
if (serverData.length === 0) {
setServerError("No servers are currently available. Please try again later.")
}
} catch (error) {
setServerError("Failed to load servers. Please check your connection and try again.")
} finally {
setIsLoadingServers(false)
}
}
loadServers()
}, [])
useEffect(() => {
if (servers.length > 0 && !isLoadingServers && !hasAutoTested) {
const autoTestPings = async () => {
setIsTestingPings(true)
setHasAutoTested(true)
setServers((prevServers) =>
prevServers.map((server) => ({
...server,
pingStatus: "testing" as const,
})),
)
try {
const pingPromises = servers.map((server) => testServerPing(server))
const results = await Promise.all(pingPromises)
const updatedServers = results.map((result) => ({
...result.server,
ping: result.ping,
pingStatus: result.status,
}))
setServers(updatedServers)
const successfulServers = updatedServers.filter((s) => s.pingStatus === "success" && s.ping !== null)
if (successfulServers.length > 0) {
const bestServer = successfulServers.reduce((prev, current) =>
prev.ping! < current.ping! ? prev : current,
)
onServerSelect(bestServer)
} else if (updatedServers.length > 0) {
onServerSelect(updatedServers[0])
}
} catch (error) {
console.error("Error testing pings:", error)
} finally {
setIsTestingPings(false)
}
}
autoTestPings()
}
}, [servers.length, isLoadingServers, hasAutoTested, onServerSelect])
const updateConfig = (updates: Partial<TunnelConfig>) => {
const newConfig = { ...localConfig, ...updates }
if (updates.serverPort && (updates.serverPort === 80 || updates.serverPort === 443)) {
newConfig.type = "http"
}
setLocalConfig(newConfig)
onConfigChange(newConfig)
}
const generateCommand = () => {
if (!selectedServer) return ""
const { serverPort, localPort } = localConfig
return `ssh ${selectedServer.subdomain} -p 2200 -R ${serverPort}:localhost:${localPort}`
}
const copyToClipboard = () => {
const command = generateCommand()
if (command) {
navigator.clipboard.writeText(command)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
}
const getPingColor = (server: Server) => {
if (server.pingStatus === "testing") return "text-gray-400"
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "text-red-400"
if (server.pingStatus === "idle" || !server.ping) return "text-gray-400"
if (server.ping < 50) return "text-green-400"
if (server.ping < 100) return "text-yellow-400"
if (server.ping < 150) return "text-orange-400"
return "text-red-400"
}
const getPingDisplay = (server: Server) => {
if (server.pingStatus === "testing") return "Testing..."
if (server.pingStatus === "timeout") return "Timeout"
if (server.pingStatus === "failed") return "Failed"
if (server.pingStatus === "idle") return "Click to test"
if (server.ping === null) return "N/A"
return `${server.ping}ms`
}
const getPingStatus = (server: Server) => {
if (server.pingStatus === "testing") return "Testing..."
if (server.pingStatus === "timeout") return "Timeout (5s)"
if (server.pingStatus === "failed") return "Connection Failed"
if (server.pingStatus === "idle") return "Not tested"
if (!server.ping) return "Unknown"
if (server.ping < 50) return "Excellent"
if (server.ping < 100) return "Good"
if (server.ping < 150) return "Fair"
return "Poor"
}
const getMarkerColor = (server: Server) => {
if (selectedServer?.id === server.id) return "#10b981"
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#ef4444"
if (server.pingStatus === "success" && server.ping !== null) {
if (server.ping < 50) return "#10b981"
if (server.ping < 100) return "#eab308"
if (server.ping < 150) return "#f97316"
return "#ef4444"
}
return "#6b7280"
}
const getMarkerStroke = (server: Server) => {
if (selectedServer?.id === server.id) return "#34d399"
if (server.pingStatus === "failed" || server.pingStatus === "timeout") return "#f87171"
if (server.pingStatus === "success" && server.ping !== null) {
if (server.ping < 50) return "#34d399"
if (server.ping < 100) return "#facc15"
if (server.ping < 150) return "#fb923c"
return "#f87171"
}
return "#9ca3af"
}
const testPingForServer = async (server: Server) => {
setServers((prevServers) =>
prevServers.map((s) => (s.id === server.id ? { ...s, pingStatus: "testing", ping: null } : s)),
)
try {
const timeoutPromise = new Promise<{ server: Server; ping: number | null; status: Server["pingStatus"] }>(
(_, reject) => {
setTimeout(() => reject(new Error("Timeout")), 5000)
},
)
const result = await Promise.race([testServerPing(server), timeoutPromise])
setServers((prevServers) =>
prevServers.map((s) => (s.id === server.id ? { ...s, ping: result.ping, pingStatus: result.status } : s)),
)
} catch (error) {
console.error("Error testing ping:", error)
setServers((prevServers) =>
prevServers.map((s) => (s.id === server.id ? { ...s, ping: null, pingStatus: "timeout" } : s)),
)
}
}
const testAllPings = async () => {
if (servers.length === 0 || isTestingPings) return
setIsTestingPings(true)
setServers((prevServers) =>
prevServers.map((server) => ({
...server,
ping: null,
pingStatus: "testing" as const,
})),
)
try {
const pingPromises = servers.map((server) => testServerPing(server))
const results = await Promise.all(pingPromises)
const updatedServers = results.map((result) => ({
...result.server,
ping: result.ping,
pingStatus: result.status,
}))
setServers(updatedServers)
} catch (error) {
console.error("Error testing pings:", error)
} finally {
setIsTestingPings(false)
}
}
return (
<div className="bg-gray-900 rounded-lg border border-gray-800 p-6 mb-8">
<h3 className="text-lg font-bold mb-6">Tunnel Configuration</h3>
<div className="mb-6">
<div className="flex items-center justify-between mb-4">
<h4 className="text-md font-medium">Choose Your Server Location</h4>
{servers.length > 0 && hasAutoTested && (
<button
onClick={testAllPings}
disabled={isTestingPings}
className="text-sm text-emerald-400 hover:text-emerald-300 disabled:text-gray-500 disabled:cursor-not-allowed flex items-center gap-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={isTestingPings ? "animate-spin" : ""}
>
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
<path d="M21 3v5h-5" />
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
<path d="M3 21v-5h5" />
</svg>
{isTestingPings ? "Testing All..." : "Test All Pings"}
</button>
)}
</div>
{isLoadingServers ? (
<div className="bg-gray-800 rounded-lg border border-gray-700 p-8 text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-emerald-400 mx-auto mb-4"></div>
<p className="text-gray-400">Loading available servers...</p>
</div>
) : serverError ? (
<div className="bg-red-950 rounded-lg border border-red-800 p-6 text-center">
<div className="mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-red-400 mx-auto"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" x2="9" y1="9" y2="15" />
<line x1="9" x2="15" y1="9" y2="15" />
</svg>
</div>
<p className="text-red-400 font-medium mb-2">Server Unavailable</p>
<p className="text-red-300 text-sm">{serverError}</p>
<button
onClick={() => window.location.reload()}
className="mt-4 px-4 py-2 bg-red-800 hover:bg-red-700 text-white rounded-lg text-sm transition-colors"
>
Retry
</button>
</div>
) : servers.length === 0 ? (
<div className="bg-yellow-950 rounded-lg border border-yellow-800 p-6 text-center">
<div className="mb-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="48"
height="48"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-yellow-400 mx-auto"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
<line x1="12" x2="12" y1="9" y2="13" />
<line x1="12" x2="12.01" y1="17" y2="17" />
</svg>
</div>
<p className="text-yellow-400 font-medium mb-2">No Servers Available</p>
<p className="text-yellow-300 text-sm">All servers are currently offline for maintenance.</p>
</div>
) : (
<>
<div className="bg-gray-800 rounded-lg border border-gray-700 p-3 mb-4">
<ComposableMap
projection="geoMercator"
projectionConfig={{
scale: 100,
center: [0, 20],
}}
width={600}
height={250}
style={{
width: "100%",
height: "auto",
}}
>
<Geographies geography={geoUrl}>
{({ geographies }) =>
geographies.map((geo) => (
<Geography
key={geo.rsmKey}
geography={geo}
fill="#374151"
stroke="#4b5563"
strokeWidth={0.5}
style={{
default: { outline: "none" },
hover: { outline: "none", fill: "#4b5563" },
pressed: { outline: "none" },
}}
/>
))
}
</Geographies>
{servers.map((server) => (
<Marker
key={server.id}
coordinates={server.coordinates}
onClick={() => {
if (server.pingStatus === "failed" || server.pingStatus === "timeout") {
return
}
onServerSelect(server)
}}
>
<g>
{selectedServer?.id === server.id && (
<circle r="12" fill="none" stroke="#10b981" strokeWidth="2" opacity="0.6">
<animate attributeName="r" values="6;15;6" dur="2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite" />
</circle>
)}
<circle r="6" fill={getMarkerColor(server)} stroke={getMarkerStroke(server)} strokeWidth="2" />
<text
textAnchor="middle"
y="-12"
style={{
fontFamily: "system-ui",
fontSize: "10px",
fontWeight: "bold",
fill: "white",
pointerEvents: "none",
}}
>
{server.location}
</text>
</g>
</Marker>
))}
</ComposableMap>
</div>
<div className="grid gap-3 md:grid-cols-3">
{servers.map((server) => (
<div
key={server.id}
onClick={() => {
if (server.pingStatus === "failed" || server.pingStatus === "timeout") {
return
}
onServerSelect(server)
}}
className={`p-3 rounded-lg border transition-all duration-200 ${selectedServer?.id === server.id
? "bg-emerald-950 border-emerald-500"
: server.pingStatus === "failed" || server.pingStatus === "timeout"
? "bg-red-950 border-red-800 cursor-not-allowed opacity-75"
: "bg-gray-800 border-gray-700 hover:border-gray-600 cursor-pointer"
}`}
>
<div className="flex items-center justify-between mb-1">
<h5 className="font-medium text-sm">{server.name}</h5>
<div
className={`w-2 h-2 rounded-full ${selectedServer?.id === server.id
? "bg-emerald-400"
: server.pingStatus === "failed" || server.pingStatus === "timeout"
? "bg-red-400"
: server.pingStatus === "success" && server.ping !== null
? server.ping < 50
? "bg-green-400"
: server.ping < 100
? "bg-yellow-400"
: server.ping < 150
? "bg-orange-400"
: "bg-red-400"
: "bg-gray-600"
}`}
/>
</div>
<p className="text-xs text-gray-400 mb-1">{server.location}</p>
<p className="text-xs font-mono text-gray-500 mb-2">{server.subdomain}</p>
<div className="flex items-center justify-between">
<span className="text-xs">Ping:</span>
<div className="flex items-center gap-1">
{server.pingStatus === "testing" && (
<div className="animate-spin rounded-full h-3 w-3 border border-gray-400 border-t-transparent"></div>
)}
{hasAutoTested ? (
<button
onClick={(e) => {
e.stopPropagation()
if (server.pingStatus !== "testing") {
testPingForServer(server)
}
}}
disabled={server.pingStatus === "testing"}
className={`text-xs font-bold hover:underline disabled:no-underline disabled:cursor-not-allowed ${getPingColor(
server,
)}`}
>
{getPingDisplay(server)}
</button>
) : (
<span className={`text-xs font-bold ${getPingColor(server)}`}>{getPingDisplay(server)}</span>
)}
</div>
</div>
<p className="text-xs text-gray-500 mt-1">{getPingStatus(server)}</p>
{(server.pingStatus === "failed" || server.pingStatus === "timeout") && (
<p className="text-xs text-red-400 mt-1 font-medium">Cannot select - Server unavailable</p>
)}
</div>
))}
</div>
</>
)}
</div>
{servers.length > 0 && (
<>
<div className="mb-6">
<label className="block text-sm font-medium mb-3">Forwarding Type</label>
<div className="flex gap-4">
<label className="flex items-center cursor-pointer">
<input
type="radio"
name="forwardingType"
value="http"
checked={localConfig.type === "http"}
onChange={() => updateConfig({ type: "http", serverPort: 443 })}
className="sr-only"
/>
<div
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${localConfig.type === "http"
? "bg-emerald-950 border-emerald-500 text-emerald-400"
: "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
}`}
>
<div
className={`w-2 h-2 rounded-full ${localConfig.type === "http" ? "bg-emerald-400" : "bg-gray-500"}`}
/>
<span className="font-medium">HTTP/HTTPS</span>
</div>
</label>
<label className="flex items-center cursor-pointer">
<input
type="radio"
name="forwardingType"
value="tcp"
checked={localConfig.type === "tcp"}
onChange={() => updateConfig({ type: "tcp", serverPort: 8080 })}
className="sr-only"
/>
<div
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-all ${localConfig.type === "tcp"
? "bg-emerald-950 border-emerald-500 text-emerald-400"
: "bg-gray-800 border-gray-700 text-gray-300 hover:border-gray-600"
}`}
>
<div
className={`w-2 h-2 rounded-full ${localConfig.type === "tcp" ? "bg-emerald-400" : "bg-gray-500"}`}
/>
<span className="font-medium">TCP</span>
</div>
</label>
</div>
<p className="text-sm text-gray-400 mt-2">
{localConfig.type === "http"
? "Best for web applications and APIs. Uses HTTPS (port 443) or HTTP (port 80)."
: "For any TCP service like databases, game servers, or custom applications."}
</p>
</div>
<div className="grid gap-4 md:grid-cols-2 mb-6">
<div>
<label className="block text-sm font-medium mb-2">Server Port (Internet Access)</label>
{localConfig.type === "http" ? (
<select
value={localConfig.serverPort}
onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) })}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white font-mono focus:border-emerald-500 focus:outline-none"
>
<option value={443}>443 (HTTPS)</option>
<option value={80}>80 (HTTP)</option>
</select>
) : (
<input
type="number"
value={localConfig.serverPort}
onChange={(e) => updateConfig({ serverPort: Number.parseInt(e.target.value) || 8080 })}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white font-mono focus:border-emerald-500 focus:outline-none"
placeholder="8080"
min="1024"
max="65535"
/>
)}
<p className="text-xs text-gray-400 mt-1">
{localConfig.type === "http" ? "Standard web ports" : "Port accessible from the internet"}
</p>
</div>
<div>
<label className="block text-sm font-medium mb-2">Local Port (Your Service)</label>
<input
type="number"
value={localConfig.localPort}
onChange={(e) => updateConfig({ localPort: Number.parseInt(e.target.value) || 8000 })}
className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white font-mono focus:border-emerald-500 focus:outline-none"
placeholder="8000"
min="1"
max="65535"
/>
<p className="text-xs text-gray-400 mt-1">Port where your local service is running</p>
</div>
</div>
{selectedServer && (
<div className="mb-6">
<label className="block text-sm font-medium mb-2">SSH Command</label>
<div className="relative">
<div className="bg-gray-800 rounded-lg p-4 border border-gray-700 font-mono text-sm overflow-x-auto">
<pre className="whitespace-pre-wrap break-all sm:break-normal">{generateCommand()}</pre>
</div>
<button
onClick={copyToClipboard}
className="absolute right-3 top-3 h-8 w-8 flex items-center justify-center rounded-md text-gray-400 hover:text-white hover:bg-gray-700 transition-colors"
aria-label="Copy command"
>
{copied ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="text-emerald-400"
>
<path d="M20 6 9 17l-5-5" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
</svg>
)}
</button>
</div>
<p className="text-xs text-gray-400 mt-2">Run this command in your terminal to create the tunnel</p>
</div>
)}
<div className="p-3 bg-gray-800 rounded-lg border border-gray-700">
<p className="text-sm text-gray-300">
<span className="font-medium">Traffic Flow:</span> Internet {" "}
<span className="text-emerald-400 font-mono">
{selectedServer ? selectedServer.location : "Server"}:{localConfig.serverPort}
</span>{" "}
<span className="text-emerald-400 font-mono">localhost:{localConfig.localPort}</span>
</p>
<p className="text-xs text-gray-400 mt-1">
{localConfig.type === "http" ? (
<>
Your local service on port {localConfig.localPort} will be accessible via{" "}
{localConfig.serverPort === 443 ? "HTTPS" : "HTTP"}
</>
) : (
<>
TCP traffic to server port {localConfig.serverPort} will be forwarded to your localhost:
{localConfig.localPort}
</>
)}
</p>
</div>
</>
)}
</div>
)
}
export type { Server }

183
components/world-map.tsx Normal file
View File

@ -0,0 +1,183 @@
"use client"
import { useState, useEffect } from "react"
import { ComposableMap, Geographies, Geography, Marker } from "react-simple-maps"
interface Server {
id: string
name: string
location: string
subdomain: string
coordinates: [number, number] // [longitude, latitude]
ping: number | null
}
const servers: Server[] = [
{
id: "sgp",
name: "Singapore",
location: "Singapore",
subdomain: "sgp.tunnl.live",
coordinates: [103.8198, 1.3521],
ping: null,
},
{
id: "id",
name: "Indonesia",
location: "Jakarta",
subdomain: "id.tunnl.live",
coordinates: [106.8456, -6.2088],
ping: null,
},
]
const geoUrl = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"
interface WorldMapProps {
onServerSelect: (server: Server) => void
selectedServer: Server
}
export default function WorldMap({ onServerSelect, selectedServer }: WorldMapProps) {
const [serverPings, setServerPings] = useState<Server[]>(servers)
const [isLoading, setIsLoading] = useState(true)
const getPingColor = (ping: number | null) => {
if (!ping) return "text-gray-400"
if (ping < 50) return "text-green-400"
if (ping < 100) return "text-yellow-400"
if (ping < 150) return "text-orange-400"
return "text-red-400"
}
const getPingStatus = (ping: number | null) => {
if (!ping) return "Testing..."
if (ping < 50) return "Excellent"
if (ping < 100) return "Good"
if (ping < 150) return "Fair"
return "Poor"
}
const getMarkerColor = (server: Server) => {
if (selectedServer.id === server.id) return "#10b981"
return "#6b7280"
}
const getMarkerStroke = (server: Server) => {
if (selectedServer.id === server.id) return "#34d399"
return "#9ca3af"
}
return (
<div className="w-full">
<h3 className="text-xl font-bold text-center mb-6">Choose Your Server Location</h3>
<div className="relative bg-gray-900 rounded-lg border border-gray-800 p-4 mb-6 overflow-hidden">
<ComposableMap
projection="geoMercator"
projectionConfig={{
scale: 120,
center: [0, 20],
}}
width={800}
height={400}
style={{
width: "100%",
height: "auto",
}}
>
<Geographies geography={geoUrl}>
{({ geographies }) =>
geographies.map((geo) => (
<Geography
key={geo.rsmKey}
geography={geo}
fill="#374151"
stroke="#4b5563"
strokeWidth={0.5}
style={{
default: { outline: "none" },
hover: { outline: "none", fill: "#4b5563" },
pressed: { outline: "none" },
}}
/>
))
}
</Geographies>
{serverPings.map((server) => (
<Marker
key={server.id}
coordinates={server.coordinates}
onClick={() => onServerSelect(server)}
>
<g>
{selectedServer.id === server.id && (
<circle r="15" fill="none" stroke="#10b981" strokeWidth="2" opacity="0.6">
<animate attributeName="r" values="8;20;8" dur="2s" repeatCount="indefinite" />
<animate attributeName="opacity" values="0.6;0;0.6" dur="2s" repeatCount="indefinite" />
</circle>
)}
<circle
r="8"
fill={getMarkerColor(server)}
stroke={getMarkerStroke(server)}
strokeWidth="2"
className="transition-all duration-200 hover:r-10"
/>
<text
textAnchor="middle"
y="-15"
style={{
fontFamily: "system-ui",
fontSize: "12px",
fontWeight: "bold",
fill: "white",
pointerEvents: "none",
}}
>
{server.location}
</text>
</g>
</Marker>
))}
</ComposableMap>
</div>
<div className="grid gap-4 md:grid-cols-3">
{serverPings.map((server) => (
<div
key={server.id}
onClick={() => onServerSelect(server)}
className={`p-4 rounded-lg border cursor-pointer transition-all duration-200 ${
selectedServer.id === server.id
? "bg-emerald-950 border-emerald-500"
: "bg-gray-900 border-gray-800 hover:border-gray-700"
}`}
>
<div className="flex items-center justify-between mb-2">
<h4 className="font-bold">{server.name}</h4>
<div
className={`w-3 h-3 rounded-full ${selectedServer.id === server.id ? "bg-emerald-400" : "bg-gray-600"}`}
/>
</div>
<p className="text-sm text-gray-400 mb-2">{server.location}</p>
<p className="text-xs font-mono text-gray-500 mb-2">{server.subdomain}</p>
<div className="flex items-center justify-between">
<span className="text-sm">Ping:</span>
{isLoading ? (
<span className="text-sm text-gray-400">Testing...</span>
) : (
<span className={`text-sm font-bold ${getPingColor(server.ping)}`}>
{server.ping}ms ({getPingStatus(server.ping)})
</span>
)}
</div>
</div>
))}
</div>
</div>
)
}

639
package-lock.json generated
View File

@ -8,9 +8,10 @@
"name": "tunnlpls_frontend",
"version": "0.1.0",
"dependencies": {
"next": "15.3.1",
"next": "^15.5.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-simple-maps": "^3.0.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
@ -18,6 +19,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-simple-maps": "^3.0.6",
"tailwindcss": "^4",
"typescript": "^5"
}
@ -36,9 +38,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"license": "MIT",
"optional": true,
"dependencies": {
@ -46,9 +48,9 @@
}
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz",
"integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
"integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
"cpu": [
"arm64"
],
@ -64,13 +66,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.1.0"
"@img/sharp-libvips-darwin-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz",
"integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
"integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
"cpu": [
"x64"
],
@ -86,13 +88,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.1.0"
"@img/sharp-libvips-darwin-x64": "1.2.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz",
"integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
"integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
"cpu": [
"arm64"
],
@ -106,9 +108,9 @@
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz",
"integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
"integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
"cpu": [
"x64"
],
@ -122,9 +124,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz",
"integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
"integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
"cpu": [
"arm"
],
@ -138,9 +140,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz",
"integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
"integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
"cpu": [
"arm64"
],
@ -154,9 +156,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz",
"integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
"integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
"cpu": [
"ppc64"
],
@ -170,9 +172,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz",
"integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
"integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
"cpu": [
"s390x"
],
@ -186,9 +188,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz",
"integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
"integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
"cpu": [
"x64"
],
@ -202,9 +204,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz",
"integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
"integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
"cpu": [
"arm64"
],
@ -218,9 +220,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz",
"integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
"integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
"cpu": [
"x64"
],
@ -234,9 +236,9 @@
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz",
"integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
"integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
"cpu": [
"arm"
],
@ -252,13 +254,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.1.0"
"@img/sharp-libvips-linux-arm": "1.2.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz",
"integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
"integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
"cpu": [
"arm64"
],
@ -274,13 +276,35 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.1.0"
"@img/sharp-libvips-linux-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
"integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz",
"integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
"integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
"cpu": [
"s390x"
],
@ -296,13 +320,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.1.0"
"@img/sharp-libvips-linux-s390x": "1.2.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz",
"integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
"integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
"cpu": [
"x64"
],
@ -318,13 +342,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.1.0"
"@img/sharp-libvips-linux-x64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz",
"integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
"integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
"cpu": [
"arm64"
],
@ -340,13 +364,13 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0"
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz",
"integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
"integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
"cpu": [
"x64"
],
@ -362,20 +386,20 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.1.0"
"@img/sharp-libvips-linuxmusl-x64": "1.2.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz",
"integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
"integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.4.0"
"@emnapi/runtime": "^1.4.4"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@ -384,10 +408,29 @@
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
"integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz",
"integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
"integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
"cpu": [
"ia32"
],
@ -404,9 +447,9 @@
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz",
"integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
"integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
"cpu": [
"x64"
],
@ -423,15 +466,15 @@
}
},
"node_modules/@next/env": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz",
"integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz",
"integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz",
"integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz",
"integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==",
"cpu": [
"arm64"
],
@ -445,9 +488,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz",
"integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz",
"integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==",
"cpu": [
"x64"
],
@ -461,9 +504,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz",
"integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz",
"integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==",
"cpu": [
"arm64"
],
@ -477,9 +520,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz",
"integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz",
"integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==",
"cpu": [
"arm64"
],
@ -493,9 +536,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz",
"integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz",
"integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==",
"cpu": [
"x64"
],
@ -509,9 +552,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz",
"integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz",
"integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==",
"cpu": [
"x64"
],
@ -525,9 +568,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz",
"integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz",
"integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==",
"cpu": [
"arm64"
],
@ -541,9 +584,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz",
"integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz",
"integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==",
"cpu": [
"x64"
],
@ -556,12 +599,6 @@
"node": ">= 10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -839,6 +876,58 @@
"tailwindcss": "4.1.5"
}
},
"node_modules/@types/d3-color": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-2.0.6.tgz",
"integrity": "sha512-tbaFGDmJWHqnenvk3QGSvD3RVwr631BjKRD7Sc7VLRgrdX5mk5hTyoeBL6rXZaeoXzmZwIl1D2HPogEdt1rHBg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-geo": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-2.0.7.tgz",
"integrity": "sha512-RIXlxPdxvX+LAZFv+t78CuYpxYag4zuw9mZc+AwfB8tZpKU90rMEn2il2ADncmeZlb7nER9dDsJpRisA3lRvjA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/d3-interpolate": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-2.0.5.tgz",
"integrity": "sha512-UINE41RDaUMbulp+bxQMDnhOi51rh5lA2dG+dWZU0UY/IwQiG/u2x8TfnWYU9+xwGdXsJoAvrBYUEQl0r91atg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-color": "^2"
}
},
"node_modules/@types/d3-selection": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-2.0.5.tgz",
"integrity": "sha512-71BorcY0yXl12S7lvb01JdaN9TpeUHBDb4RRhSq8U8BEkX/nIk5p7Byho+ZRTsx5nYLMpAbY3qt5EhqFzfGJlw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/d3-zoom": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-2.0.7.tgz",
"integrity": "sha512-JWke4E8ZyrKUQ68ESTWSK16fVb0OYnaiJ+WXJRYxKLn4aXU0o4CLYxMWBEiouUfO3TTCoyroOrGPcBG6u1aAxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/d3-interpolate": "^2",
"@types/d3-selection": "^2"
}
},
"node_modules/@types/geojson": {
"version": "7946.0.16",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "20.17.32",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.32.tgz",
@ -869,15 +958,17 @@
"@types/react": "^19.0.0"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"node_modules/@types/react-simple-maps": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/@types/react-simple-maps/-/react-simple-maps-3.0.6.tgz",
"integrity": "sha512-hR01RXt6VvsE41FxDd+Bqm1PPGdKbYjCYVtCgh38YeBPt46z3SwmWPWu2L3EdCAP6bd6VYEgztucihRw1C0Klg==",
"dev": true,
"license": "MIT",
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
"@types/d3-geo": "^2",
"@types/d3-zoom": "^2",
"@types/geojson": "*",
"@types/react": "*"
}
},
"node_modules/caniuse-lite": {
@ -951,6 +1042,12 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -958,6 +1055,135 @@
"dev": true,
"license": "MIT"
},
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-dispatch": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-drag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-selection": "3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-ease": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-geo": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2.5.0 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-transition": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
"license": "ISC",
"dependencies": {
"d3-color": "1 - 3",
"d3-dispatch": "1 - 3",
"d3-ease": "1 - 3",
"d3-interpolate": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"d3-selection": "2 - 3"
}
},
"node_modules/d3-zoom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-drag": "2 - 3",
"d3-interpolate": "1 - 3",
"d3-selection": "2 - 3",
"d3-transition": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
@ -989,6 +1215,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@ -1006,6 +1241,13 @@
"jiti": "lib/jiti-cli.mjs"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT",
"peer": true
},
"node_modules/lightningcss": {
"version": "1.29.2",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
@ -1245,6 +1487,19 @@
"url": "https://opencollective.com/parcel"
}
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@ -1264,15 +1519,13 @@
}
},
"node_modules/next": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz",
"integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==",
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz",
"integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==",
"license": "MIT",
"dependencies": {
"@next/env": "15.3.1",
"@swc/counter": "0.1.3",
"@next/env": "15.5.2",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
@ -1284,19 +1537,19 @@
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.3.1",
"@next/swc-darwin-x64": "15.3.1",
"@next/swc-linux-arm64-gnu": "15.3.1",
"@next/swc-linux-arm64-musl": "15.3.1",
"@next/swc-linux-x64-gnu": "15.3.1",
"@next/swc-linux-x64-musl": "15.3.1",
"@next/swc-win32-arm64-msvc": "15.3.1",
"@next/swc-win32-x64-msvc": "15.3.1",
"sharp": "^0.34.1"
"@next/swc-darwin-arm64": "15.5.2",
"@next/swc-darwin-x64": "15.5.2",
"@next/swc-linux-arm64-gnu": "15.5.2",
"@next/swc-linux-arm64-musl": "15.5.2",
"@next/swc-linux-x64-gnu": "15.5.2",
"@next/swc-linux-x64-musl": "15.5.2",
"@next/swc-win32-arm64-msvc": "15.5.2",
"@next/swc-win32-x64-msvc": "15.5.2",
"sharp": "^0.34.3"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"@playwright/test": "^1.51.1",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
@ -1345,6 +1598,16 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@ -1380,6 +1643,18 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
@ -1401,6 +1676,30 @@
"react": "^19.1.0"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT",
"peer": true
},
"node_modules/react-simple-maps": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/react-simple-maps/-/react-simple-maps-3.0.0.tgz",
"integrity": "sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==",
"license": "MIT",
"dependencies": {
"d3-geo": "^2.0.2",
"d3-selection": "^2.0.0",
"d3-zoom": "^2.0.0",
"topojson-client": "^3.1.0"
},
"peerDependencies": {
"prop-types": "^15.7.2",
"react": "^16.8.0 || 17.x || 18.x",
"react-dom": "^16.8.0 || 17.x || 18.x"
}
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
@ -1408,9 +1707,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"optional": true,
"bin": {
@ -1421,16 +1720,16 @@
}
},
"node_modules/sharp": {
"version": "0.34.1",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz",
"integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==",
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
"integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.7.1"
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@ -1439,26 +1738,28 @@
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.1",
"@img/sharp-darwin-x64": "0.34.1",
"@img/sharp-libvips-darwin-arm64": "1.1.0",
"@img/sharp-libvips-darwin-x64": "1.1.0",
"@img/sharp-libvips-linux-arm": "1.1.0",
"@img/sharp-libvips-linux-arm64": "1.1.0",
"@img/sharp-libvips-linux-ppc64": "1.1.0",
"@img/sharp-libvips-linux-s390x": "1.1.0",
"@img/sharp-libvips-linux-x64": "1.1.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.1.0",
"@img/sharp-libvips-linuxmusl-x64": "1.1.0",
"@img/sharp-linux-arm": "0.34.1",
"@img/sharp-linux-arm64": "0.34.1",
"@img/sharp-linux-s390x": "0.34.1",
"@img/sharp-linux-x64": "0.34.1",
"@img/sharp-linuxmusl-arm64": "0.34.1",
"@img/sharp-linuxmusl-x64": "0.34.1",
"@img/sharp-wasm32": "0.34.1",
"@img/sharp-win32-ia32": "0.34.1",
"@img/sharp-win32-x64": "0.34.1"
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-libvips-darwin-arm64": "1.2.0",
"@img/sharp-libvips-darwin-x64": "1.2.0",
"@img/sharp-libvips-linux-arm": "1.2.0",
"@img/sharp-libvips-linux-arm64": "1.2.0",
"@img/sharp-libvips-linux-ppc64": "1.2.0",
"@img/sharp-libvips-linux-s390x": "1.2.0",
"@img/sharp-libvips-linux-x64": "1.2.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
"@img/sharp-libvips-linuxmusl-x64": "1.2.0",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-ppc64": "0.34.3",
"@img/sharp-linux-s390x": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-linuxmusl-arm64": "0.34.3",
"@img/sharp-linuxmusl-x64": "0.34.3",
"@img/sharp-wasm32": "0.34.3",
"@img/sharp-win32-arm64": "0.34.3",
"@img/sharp-win32-ia32": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
}
},
"node_modules/simple-swizzle": {
@ -1480,14 +1781,6 @@
"node": ">=0.10.0"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@ -1536,6 +1829,20 @@
"node": ">=6"
}
},
"node_modules/topojson-client": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
"license": "ISC",
"dependencies": {
"commander": "2"
},
"bin": {
"topo2geo": "bin/topo2geo",
"topomerge": "bin/topomerge",
"topoquantize": "bin/topoquantize"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",

View File

@ -2,6 +2,22 @@
"name": "tunnlpls_frontend",
"version": "0.1.0",
"private": true,
"overrides": {
"react-simple-maps": {
"d3-geo": "^3.1.0",
"d3-color": "^3.1.0",
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0",
"react": "^16.8.0 || 17.x || 18.x || 19.x",
"react-dom": "^16.8.0 || 17.x || 18.x || 19.x"
},
"d3-interpolate": {
"d3-color": "^3.1.0"
},
"d3-transition": {
"d3-color": "^3.1.0"
}
},
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
@ -9,9 +25,10 @@
"lint": "next lint"
},
"dependencies": {
"next": "15.3.1",
"next": "^15.5.2",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-simple-maps": "^3.0.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
@ -19,6 +36,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/react-simple-maps": "^3.0.6",
"tailwindcss": "^4",
"typescript": "^5"
}