diff --git a/package-lock.json b/package-lock.json index 1935356..5937d02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "clickhouse": "^2.6.0", "clsx": "^2.1.1", "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", "lucide-react": "^0.525.0", "next": "15.3.4", "next-themes": "^0.4.6", @@ -4420,6 +4421,14 @@ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==" }, + "node_modules/leaflet.markercluster": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.5.3.tgz", + "integrity": "sha512-vPTw/Bndq7eQHjLBVlWpnGeLa3t+3zGiuM7fJwCkiMFq+nmRuG3RI3f7f4N4TDX7T4NpbAXpR2+NTRSEGfCSeA==", + "peerDependencies": { + "leaflet": "^1.3.1" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index f681301..6b1935b 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "clickhouse": "^2.6.0", "clsx": "^2.1.1", "leaflet": "^1.9.4", + "leaflet.markercluster": "^1.5.3", "lucide-react": "^0.525.0", "next": "15.3.4", "next-themes": "^0.4.6", diff --git a/src/app/api/node-positions/route.ts b/src/app/api/node-positions/route.ts new file mode 100644 index 0000000..0b0decc --- /dev/null +++ b/src/app/api/node-positions/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import { getNodePositions } from "@/lib/clickhouse/actions"; + +export async function GET(req: Request) { + try { + const { searchParams } = new URL(req.url); + const minLat = searchParams.get("minLat"); + const maxLat = searchParams.get("maxLat"); + const minLng = searchParams.get("minLng"); + const maxLng = searchParams.get("maxLng"); + const positions = await getNodePositions({ minLat, maxLat, minLng, maxLng }); + return NextResponse.json(positions); + } catch (error) { + return NextResponse.json({ error: "Failed to fetch node positions" }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index 9711daf..5eec4e2 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -120,3 +120,57 @@ @apply bg-background text-foreground; } } + +.custom-node-marker { + width: 16px; + height: 16px; + background: #2563eb; /* blue-600 */ + border: 2px solid #fff; + border-radius: 50%; + box-shadow: 0 0 4px rgba(0,0,0,0.2); + display: block; + position: relative; + z-index: 2; +} + +.custom-node-marker--green { + background: #22c55e; /* green-500 */ +} +.custom-node-marker--blue { + background: #2563eb; /* blue-600 */ +} + +.custom-node-marker--top { + z-index: 1002 !important; + position: relative; +} + +.map-spinner { + width: 28px; + height: 28px; + border: 4px solid #e5e7eb; /* gray-200 */ + border-top: 4px solid #2563eb; /* blue-600 */ + border-radius: 50%; + animation: map-spin 1s linear infinite; +} +@keyframes map-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.custom-node-label { + position: absolute; + top: 18px; + left: 50%; + transform: translateX(-50%); + font-size: 12px; + color: #222; + background: rgba(255,255,255,0.85); + padding: 0 4px; + border-radius: 3px; + white-space: nowrap; + pointer-events: none; + font-weight: 500; + box-shadow: 0 1px 2px rgba(0,0,0,0.07); + z-index: 1; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..bf43a87 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import Header from "../components/Header"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -26,8 +27,12 @@ export default function RootLayout({
- {children} ++ Sorry, the page you are looking for does not exist or has been moved. +
+ + Go Home + +