diff --git a/.gitignore b/.gitignore
index 5ef6a52..3c3629e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,41 +1 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.*
-.yarn/*
-!.yarn/patches
-!.yarn/plugins
-!.yarn/releases
-!.yarn/versions
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# env files (can opt-in for committing if needed)
-.env*
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
+node_modules
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..1b3be08
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/package-lock.json b/package-lock.json
index 6426661..a4d5d70 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,11 +9,15 @@
"version": "0.1.0",
"dependencies": {
"@prisma/client": "^6.4.1",
+ "@types/mapbox-gl": "^3.4.1",
+ "leaflet": "^1.9.4",
+ "mapbox-gl": "^3.10.0",
"next": "15.1.7",
"prisma": "^6.4.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
+ "react-leaflet": "^5.0.0",
"react-node": "^1.0.2"
},
"devDependencies": {
@@ -1077,6 +1081,56 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@mapbox/jsonlint-lines-primitives": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
+ "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@mapbox/mapbox-gl-supported": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz",
+ "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@mapbox/point-geometry": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
+ "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
+ "license": "ISC"
+ },
+ "node_modules/@mapbox/tiny-sdf": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
+ "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/unitbezier": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
+ "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/@mapbox/vector-tile": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
+ "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@mapbox/point-geometry": "~0.1.0"
+ }
+ },
+ "node_modules/@mapbox/whoots-js": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
+ "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@next/env": {
"version": "15.1.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.7.tgz",
@@ -1347,6 +1401,17 @@
"@prisma/debug": "6.4.1"
}
},
+ "node_modules/@react-leaflet/core": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-3.0.0.tgz",
+ "integrity": "sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==",
+ "license": "Hippocratic-2.1",
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -1383,6 +1448,21 @@
"dev": true,
"license": "MIT"
},
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/@types/geojson-vt": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
+ "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1397,6 +1477,32 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/mapbox__point-geometry": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
+ "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/mapbox__vector-tile": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
+ "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*",
+ "@types/mapbox__point-geometry": "*",
+ "@types/pbf": "*"
+ }
+ },
+ "node_modules/@types/mapbox-gl": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz",
+ "integrity": "sha512-NsGKKtgW93B+UaLPti6B7NwlxYlES5DpV5Gzj9F75rK5ALKsqSk15CiEHbOnTr09RGbr6ZYiCdI+59NNNcAImg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/node": {
"version": "20.17.19",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
@@ -1407,6 +1513,12 @@
"undici-types": "~6.19.2"
}
},
+ "node_modules/@types/pbf": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
+ "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
+ "license": "MIT"
+ },
"node_modules/@types/react": {
"version": "19.0.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz",
@@ -1427,6 +1539,15 @@
"@types/react": "^19.0.0"
}
},
+ "node_modules/@types/supercluster": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
+ "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.24.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.1.tgz",
@@ -2174,6 +2295,12 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/cheap-ruler": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz",
+ "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==",
+ "license": "ISC"
+ },
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -2340,6 +2467,12 @@
"node": ">= 8"
}
},
+ "node_modules/csscolorparser": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz",
+ "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==",
+ "license": "MIT"
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -2564,6 +2697,12 @@
"node": ">= 0.4"
}
},
+ "node_modules/earcut": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz",
+ "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==",
+ "license": "ISC"
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -3507,6 +3646,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/geojson-vt": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
+ "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
+ "license": "ISC"
+ },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -3577,6 +3722,12 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
+ "node_modules/gl-matrix": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
+ "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
+ "license": "MIT"
+ },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -3693,6 +3844,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/grid-index": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz",
+ "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==",
+ "license": "ISC"
+ },
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
@@ -3799,6 +3956,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4442,6 +4619,12 @@
"node": ">=4.0"
}
},
+ "node_modules/kdbush": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
+ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
+ "license": "ISC"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -4472,6 +4655,12 @@
"node": ">=0.10"
}
},
+ "node_modules/leaflet": {
+ "version": "1.9.4",
+ "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
+ "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -4549,6 +4738,46 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/mapbox-gl": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.10.0.tgz",
+ "integrity": "sha512-YnQxjlthuv/tidcxGYU2C8nRDVXMlAHa3qFhuOJeX4AfRP72OMRBf9ApL+M+k5VWcAXi2fcNOUVgphknjLumjA==",
+ "license": "SEE LICENSE IN LICENSE.txt",
+ "workspaces": [
+ "src/style-spec",
+ "test/build/typings"
+ ],
+ "dependencies": {
+ "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+ "@mapbox/mapbox-gl-supported": "^3.0.0",
+ "@mapbox/point-geometry": "^0.1.0",
+ "@mapbox/tiny-sdf": "^2.0.6",
+ "@mapbox/unitbezier": "^0.0.1",
+ "@mapbox/vector-tile": "^1.3.1",
+ "@mapbox/whoots-js": "^3.1.0",
+ "@types/geojson": "^7946.0.16",
+ "@types/geojson-vt": "^3.2.5",
+ "@types/mapbox__point-geometry": "^0.1.4",
+ "@types/mapbox__vector-tile": "^1.3.4",
+ "@types/pbf": "^3.0.5",
+ "@types/supercluster": "^7.1.3",
+ "cheap-ruler": "^4.0.0",
+ "csscolorparser": "~1.0.3",
+ "earcut": "^3.0.0",
+ "geojson-vt": "^4.0.2",
+ "gl-matrix": "^3.4.3",
+ "grid-index": "^1.1.0",
+ "kdbush": "^4.0.2",
+ "murmurhash-js": "^1.0.0",
+ "pbf": "^3.2.1",
+ "potpack": "^2.0.0",
+ "quickselect": "^3.0.0",
+ "serialize-to-js": "^3.1.2",
+ "supercluster": "^8.0.1",
+ "tinyqueue": "^3.0.0",
+ "vt-pbf": "^3.1.3"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -4632,6 +4861,12 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
+ "node_modules/murmurhash-js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
+ "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
+ "license": "MIT"
+ },
"node_modules/mz": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@@ -5043,6 +5278,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/pbf": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
+ "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ieee754": "^1.1.12",
+ "resolve-protobuf-schema": "^2.1.0"
+ },
+ "bin": {
+ "pbf": "bin/pbf"
+ }
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -5242,6 +5490,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/potpack": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
+ "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
+ "license": "ISC"
+ },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5302,6 +5556,12 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/protocol-buffers-schema": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
+ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -5344,6 +5604,12 @@
],
"license": "MIT"
},
+ "node_modules/quickselect": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
+ "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
+ "license": "ISC"
+ },
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
@@ -5381,6 +5647,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-leaflet": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-5.0.0.tgz",
+ "integrity": "sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==",
+ "license": "Hippocratic-2.1",
+ "dependencies": {
+ "@react-leaflet/core": "^3.0.0"
+ },
+ "peerDependencies": {
+ "leaflet": "^1.9.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0"
+ }
+ },
"node_modules/react-node": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/react-node/-/react-node-1.0.2.tgz",
@@ -5534,6 +5814,15 @@
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
},
+ "node_modules/resolve-protobuf-schema": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
+ "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "protocol-buffers-schema": "^3.3.1"
+ }
+ },
"node_modules/reusify": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -5649,6 +5938,15 @@
"node": ">=10"
}
},
+ "node_modules/serialize-to-js": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/serialize-to-js/-/serialize-to-js-3.1.2.tgz",
+ "integrity": "sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
@@ -6182,6 +6480,15 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/supercluster": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
+ "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
+ "license": "ISC",
+ "dependencies": {
+ "kdbush": "^4.0.2"
+ }
+ },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -6360,6 +6667,12 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tinyqueue": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
+ "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
+ "license": "ISC"
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -6560,6 +6873,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/vt-pbf": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
+ "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
+ "license": "MIT",
+ "dependencies": {
+ "@mapbox/point-geometry": "0.1.0",
+ "@mapbox/vector-tile": "^1.3.1",
+ "pbf": "^3.2.1"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index c8c406c..f7b4242 100644
--- a/package.json
+++ b/package.json
@@ -11,11 +11,15 @@
},
"dependencies": {
"@prisma/client": "^6.4.1",
+ "@types/mapbox-gl": "^3.4.1",
+ "leaflet": "^1.9.4",
+ "mapbox-gl": "^3.10.0",
"next": "15.1.7",
"prisma": "^6.4.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
+ "react-leaflet": "^5.0.0",
"react-node": "^1.0.2"
},
"devDependencies": {
diff --git a/public/.DS_Store b/public/.DS_Store
new file mode 100644
index 0000000..63b3f4a
Binary files /dev/null and b/public/.DS_Store differ
diff --git a/src/app/earthquakes/page.tsx b/src/app/earthquakes/page.tsx
index 921e4cb..b7bed75 100644
--- a/src/app/earthquakes/page.tsx
+++ b/src/app/earthquakes/page.tsx
@@ -1,10 +1,77 @@
-import Sidebar from "@/components/sidebar_e";
+"use client";
+
+import Sidebar from "@components/Sidebar";
+import Map from "@components/Map";
+import { useState, useMemo } from "react";
export default function Earthquakes() {
+ const [selectedEventId, setSelectedEventId] = useState("");
+ const [hoveredEventId, setHoveredEventId] = useState("");
+
+ const events = useMemo(
+ () => [
+ {
+ id: "1234",
+ title: "Earthquake in Germany",
+ text1: "Magnitude 8.5",
+ text2: "30 minutes ago",
+ magnitude: 8.5,
+ longitude: 10.4515, // Near Berlin, Germany
+ latitude: 52.52,
+ },
+ {
+ id: "2134",
+ title: "Earthquake in California",
+ text1: "Magnitude 5.3",
+ text2: "2 hours ago",
+ magnitude: 5.3,
+ longitude: -122.4194, // Near San Francisco, California, USA
+ latitude: 37.7749,
+ },
+ {
+ id: "2314",
+ title: "Tremor in Japan",
+ text1: "Magnitude 4.7",
+ text2: "5 hours ago",
+ magnitude: 4.7,
+ longitude: 139.6917, // Near Tokyo, Japan
+ latitude: 35.6762,
+ },
+ {
+ id: "2341",
+ title: "Tremor in Spain",
+ text1: "Magnitude 2.1",
+ text2: "10 hours ago",
+ magnitude: 2.1,
+ longitude: -3.7038, // Near Madrid, Spain
+ latitude: 40.4168,
+ },
+ ],
+ []
+ );
+
return (
-
-
Earthquakes
-
+
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 968f6ad..48e87e9 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -18,3 +18,26 @@ body {
color: var(--foreground);
background: var(--background);
}
+
+/* Increase specificity and use !important where necessary */
+.mapboxgl-popup .mapboxgl-popup-content {
+ @apply rounded-xl p-4 px-5 drop-shadow-lg border border-neutral-300 max-w-xs !important;
+}
+
+/* Hide the popup tip */
+.mapboxgl-popup .mapboxgl-popup-tip {
+ display: none !important;
+}
+
+/* Child elements */
+.mapboxgl-popup-content h3 {
+ @apply text-sm font-medium text-neutral-800 !important;
+}
+
+.mapboxgl-popup-content p {
+ @apply text-xs text-neutral-600 !important;
+}
+
+.mapboxgl-popup-content p + p {
+ @apply text-neutral-500 !important;
+}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 5572567..c086fe6 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
-import Navbar from "@/components/navbar";
+import Navbar from "@components/Navbar";
import { Inter } from "next/font/google";
import "./globals.css";
@@ -20,9 +20,9 @@ export default function RootLayout({
}>) {
return (
-
+
- {children}
+
{children}
);
diff --git a/src/app/observatories/page.tsx b/src/app/observatories/page.tsx
index 173b280..2e17d80 100644
--- a/src/app/observatories/page.tsx
+++ b/src/app/observatories/page.tsx
@@ -1,4 +1,4 @@
-import Sidebar from "@/components/sidebar_o";
+import Sidebar from "@components/sidebar_o";
export default function Observatories() {
return (
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 0b61962..980b12e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -8,11 +8,13 @@ export default function Home() {
-
+
+
{["Earthquake 1", "Earthquake 2", "Earthquake 3"].map((name) => (
diff --git a/src/components/InteractiveMap.tsx b/src/components/InteractiveMap.tsx
deleted file mode 100644
index 27870ac..0000000
--- a/src/components/InteractiveMap.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import InteractiveMap from "@/components/InteractiveMap";
-
-export default function Home() {
- return (
-
-
Interactive Map
- {/* Render InteractiveMap */}
-
-
- );
-}
\ No newline at end of file
diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx
new file mode 100644
index 0000000..5ce8b5f
--- /dev/null
+++ b/src/components/Sidebar.tsx
@@ -0,0 +1,89 @@
+import React from "react";
+import { Dispatch, SetStateAction } from "react";
+import Link from "next/link";
+import { TbHexagon } from "react-icons/tb";
+import Event from "@appTypes/Event";
+import getMagnitudeColor from "@utils/getMagnitudeColour";
+
+interface SidebarProps {
+ logTitle: string;
+ logSubtitle: string;
+ recentsTitle: string;
+ events: Event[];
+ selectedEventId: Event["id"];
+ setSelectedEventId: Dispatch
>;
+ hoveredEventId: Event["id"];
+ setHoveredEventId: Dispatch>;
+}
+
+function MagnitudeNumber({ magnitude }: { magnitude: number }) {
+ // Convert magnitude to string with one decimal place
+ const magnitudeStr = magnitude.toFixed(1);
+ const [whole, decimal] = magnitudeStr.split(".");
+
+ // Define color based on magnitude (0-10 scale)
+ return (
+
+
+
+
+ {whole}
+ .
+ {decimal}
+
+
+
+ );
+}
+
+export default function Sidebar({
+ logTitle,
+ logSubtitle,
+ recentsTitle,
+ events,
+ selectedEventId,
+ setSelectedEventId,
+ hoveredEventId,
+ setHoveredEventId,
+}: SidebarProps) {
+ return (
+
+
+
{logTitle}
+
{logSubtitle}
+
+
+
+
+
+
+
{recentsTitle}
+
+ {events.map((event) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/map.tsx b/src/components/map.tsx
index 6464349..ff2b88a 100644
--- a/src/components/map.tsx
+++ b/src/components/map.tsx
@@ -1,39 +1,203 @@
-import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
-import "leaflet/dist/leaflet.css"; // Leaflet CSS
-import React from "react";
+import { useCallback, useState } from "react";
+import React, { useRef, useEffect, Dispatch, SetStateAction } from "react";
+import mapboxgl, { LngLatBounds } from "mapbox-gl";
+import "mapbox-gl/dist/mapbox-gl.css";
+import Event from "@appTypes/Event";
+import getMagnitudeColor from "@utils/getMagnitudeColour";
-const points = [
- { id: 1, name: "Point A", position: [51.505, -0.09] },
- { id: 2, name: "Point B", position: [51.515, -0.1] },
- { id: 3, name: "Point C", position: [51.525, -0.11] },
-];
+interface MapComponentProps {
+ events: Event[];
+ selectedEventId: Event["id"];
+ setSelectedEventId: Dispatch>;
+ hoveredEventId: Event["id"];
+ setHoveredEventId: Dispatch>;
+}
-const InteractiveMap = () => {
- return (
-
-
- {/* Tile Layer for map background */}
-
+// Map component with location-style pulsing dots, animations, and tooltips
+function MapComponent({ events, selectedEventId, setSelectedEventId, hoveredEventId, setHoveredEventId }: MapComponentProps) {
+ const map = useRef(null);
+ const markers = useRef<{ [key: string]: mapboxgl.Marker }>({});
+ const [mapBounds, setMapBounds] = useState();
- {/* Add markers for points */}
- {points.map((point) => (
-
- {/* Popup when marker is clicked */}
- {point.name}
- {/* Optional hover tooltip */}
-
- ))}
-
-
- );
-};
+ const fitToBounds = useCallback((bounds: LngLatBounds) => {
+ if (map.current && bounds) {
+ map.current!.fitBounds(bounds, { padding: 150, maxZoom: 10 });
+ }
+ }, []);
-export default InteractiveMap;
\ No newline at end of file
+ const showPopup = useCallback((eventId: string) => {
+ const marker = markers.current[eventId];
+ if (marker && map.current) {
+ marker.getPopup()?.addTo(map.current);
+ }
+ }, []);
+
+ const clearAllPopups = useCallback(() => {
+ Object.values(markers.current).forEach((marker) => marker.getPopup()?.remove());
+ }, []);
+
+ const flyToEvent = useCallback((event: Event) => {
+ if (map.current) {
+ map.current.flyTo({
+ center: [event.longitude, event.latitude],
+ zoom: 4,
+ speed: 1.5,
+ curve: 1.42,
+ });
+ }
+ }, []);
+
+ useEffect(() => {
+ mapboxgl.accessToken = "pk.eyJ1IjoidGltaG93aXR6IiwiYSI6ImNtOGtjcXA5bDA3Ym4ya3NnOWxjbjlxZG8ifQ.6u_KgXEdLTakz910QRAorQ";
+
+ try {
+ map.current = new mapboxgl.Map({
+ container: "map-container",
+ style: "mapbox://styles/mapbox/light-v10",
+ center: [0, 0],
+ zoom: 1,
+ });
+ } catch (error) {
+ console.error("Map initialization failed:", error);
+ return;
+ }
+
+ map.current.on("load", () => {
+ // Fit map to bounds
+ const bounds = new mapboxgl.LngLatBounds();
+ events.forEach((event) => {
+ bounds.extend([event.longitude, event.latitude]);
+ });
+ fitToBounds(bounds);
+ setMapBounds(bounds);
+
+ // Add markers with location pulse
+ events.forEach((event) => {
+ const color = getMagnitudeColor(event.magnitude);
+
+ // Create marker container
+ const markerElement = document.createElement("div");
+ markerElement.style.width = "20px"; // Increased size to accommodate pulse
+ markerElement.style.height = "20px";
+ markerElement.style.position = "absolute";
+ markerElement.style.display = "flex";
+ markerElement.style.alignItems = "center";
+ markerElement.style.justifyContent = "center";
+
+ // Central dot
+ const dotElement = document.createElement("div");
+ dotElement.style.width = "8px";
+ dotElement.style.height = "8px";
+ dotElement.style.backgroundColor = color;
+ dotElement.style.borderRadius = "50%";
+ dotElement.style.position = "absolute";
+ dotElement.style.zIndex = "2"; // Ensure dot is above pulse
+
+ // Pulsing ring
+ const pulseElement = document.createElement("div");
+ pulseElement.className = "location-pulse";
+ pulseElement.style.width = "20px"; // Initial size
+ pulseElement.style.height = "20px";
+ pulseElement.style.backgroundColor = `${color}80`; // Color with 50% opacity (hex alpha)
+ pulseElement.style.borderRadius = "50%";
+ pulseElement.style.position = "absolute";
+ pulseElement.style.zIndex = "1";
+
+ markerElement.appendChild(pulseElement);
+ markerElement.appendChild(dotElement);
+
+ const marker = new mapboxgl.Marker({ element: markerElement }).setLngLat([event.longitude, event.latitude]).addTo(map.current!);
+
+ const popup = new mapboxgl.Popup({ offset: 25, closeButton: false, anchor: "bottom" }).setHTML(`
+
+
${event.title}
+
Magnitude: ${event.magnitude}
+
${event.text2}
+
+ `);
+
+ marker.setPopup(popup);
+ markers.current[event.id] = marker;
+
+ // Add hover events
+ const markerDomElement = marker.getElement();
+ markerDomElement.style.cursor = "pointer"; // Optional: indicate interactivity
+
+ markerDomElement.addEventListener("mouseenter", () => setHoveredEventId(event.id));
+ markerDomElement.addEventListener("mouseleave", () => setHoveredEventId(""));
+ markerDomElement.addEventListener("click", () => setSelectedEventId((prevEventId) => (prevEventId === event.id ? "" : event.id)));
+
+ // Cleanup event listeners on unmount
+ markerDomElement.dataset.listenersAdded = "true"; // Mark for cleanup
+ });
+ });
+
+ map.current.on("error", (e) => {
+ console.error("Mapbox error:", e);
+ });
+
+ return () => {
+ map.current?.remove();
+ };
+ }, [events, setSelectedEventId, setHoveredEventId, fitToBounds]);
+
+ useEffect(() => {
+ const event = events.find((x) => x.id === selectedEventId);
+ if (event) flyToEvent(event);
+ else if (!selectedEventId) {
+ if (mapBounds) fitToBounds(mapBounds);
+ }
+ }, [events, selectedEventId, mapBounds, fitToBounds, clearAllPopups, flyToEvent]);
+
+ useEffect(() => {
+ // Clear all popups first
+ clearAllPopups();
+
+ // Handle both events if they exist and are different
+ if (hoveredEventId && selectedEventId && hoveredEventId !== selectedEventId) {
+ showPopup(hoveredEventId);
+ showPopup(selectedEventId);
+ }
+ // Handle single event case (either hovered or selected)
+ else if (hoveredEventId || selectedEventId) {
+ showPopup(hoveredEventId || selectedEventId);
+ }
+ }, [hoveredEventId, selectedEventId, clearAllPopups, showPopup]);
+
+ return (
+
+ );
+}
+
+// CSS for location-style pulsing animation
+const pulseStyles = `
+ .location-pulse {
+ animation: locationPulse 2s infinite;
+ }
+
+ @keyframes locationPulse {
+ 0% {
+ transform: scale(0.5);
+ opacity: 0.8;
+ }
+ 70% {
+ transform: scale(2);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(2);
+ opacity: 0;
+ }
+ }
+`;
+
+export default function Map(props: MapComponentProps) {
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx
index c8d3480..3a670b1 100644
--- a/src/components/navbar.tsx
+++ b/src/components/navbar.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
-import AuthModal from "@/components/AuthModal";
+import AuthModal from "@components/AuthModal";
import { usePathname } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
diff --git a/src/types/Event.ts b/src/types/Event.ts
new file mode 100644
index 0000000..02a501f
--- /dev/null
+++ b/src/types/Event.ts
@@ -0,0 +1,10 @@
+interface Event {
+ id: string;
+ title: string;
+ magnitude: number;
+ longitude: number;
+ latitude: number;
+ text2: string;
+}
+
+export default Event;
diff --git a/src/utils/getMagnitudeColour.tsx b/src/utils/getMagnitudeColour.tsx
new file mode 100644
index 0000000..5a52eb3
--- /dev/null
+++ b/src/utils/getMagnitudeColour.tsx
@@ -0,0 +1,20 @@
+"use client";
+import resolveConfig from "tailwindcss/resolveConfig";
+import tailwindConfig from "../../tailwind.config";
+
+function getMagnitudeColour(magnitude: number) {
+ const fullConfig = resolveConfig(tailwindConfig);
+ const colors = fullConfig.theme.colors;
+
+ if (magnitude >= 7) {
+ return colors["red"][600];
+ } else if (magnitude >= 5) {
+ return colors["orange"][500];
+ } else if (magnitude >= 3) {
+ return colors["amber"][500];
+ } else {
+ return colors["blue"][400];
+ }
+}
+
+export default getMagnitudeColour;
diff --git a/tsconfig.json b/tsconfig.json
index 03c4ffc..a4ead61 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,6 @@
{
"compilerOptions": {
- "moduleResolution": "node", // Use "node" module resolution strategy
+ "moduleResolution": "node", // Use "node" module resolution strategy
"forceConsistentCasingInFileNames": true,
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
@@ -20,7 +20,11 @@
}
],
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["./src/*"],
+ "@components/*": ["./src/components/*"],
+ "@hooks/*": ["./src/hooks/*"],
+ "@utils/*": ["./src/utils/*"],
+ "@appTypes/*": ["./src/types/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],