Add interactive school map to homepage
Some checks failed
Build and Deploy / deploy (push) Has been cancelled
Some checks failed
Build and Deploy / deploy (push) Has been cancelled
Replace the text list of schools with a Leaflet map showing anonymous orange pins for 110 customer schools across Australia. Includes build-time Airtable fetch script with Nominatim geocoding and gradient glow effect on clustered pins. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
52
package-lock.json
generated
52
package-lock.json
generated
@@ -9,14 +9,17 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-mdx-remote": "^6.0.0",
|
"next-mdx-remote": "^6.0.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"stripe": "^20.4.0"
|
"stripe": "^20.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
@@ -1282,6 +1285,17 @@
|
|||||||
"node": ">=12.4.0"
|
"node": ">=12.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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": {
|
"node_modules/@rtsao/scc": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||||
@@ -1604,6 +1618,13 @@
|
|||||||
"@types/estree": "*"
|
"@types/estree": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/hast": {
|
"node_modules/@types/hast": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
@@ -1627,6 +1648,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/leaflet": {
|
||||||
|
"version": "1.9.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
|
||||||
|
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/mdast": {
|
"node_modules/@types/mdast": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||||
@@ -5126,6 +5157,13 @@
|
|||||||
"node": ">=0.10"
|
"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",
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/levn": {
|
"node_modules/levn": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
||||||
@@ -6885,6 +6923,20 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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/recma-build-jsx": {
|
"node_modules/recma-build-jsx": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz",
|
||||||
|
|||||||
@@ -6,18 +6,22 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"fetch-schools": "npx tsx scripts/fetch-schools.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
|
"leaflet": "^1.9.4",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-mdx-remote": "^6.0.0",
|
"next-mdx-remote": "^6.0.0",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
|
"react-leaflet": "^5.0.0",
|
||||||
"stripe": "^20.4.0"
|
"stripe": "^20.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@types/leaflet": "^1.9.21",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
|||||||
661
public/leaflet.css
Normal file
661
public/leaflet.css
Normal file
@@ -0,0 +1,661 @@
|
|||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-pane > svg,
|
||||||
|
.leaflet-pane > canvas,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
/* Prevents IE11 from highlighting tiles in blue */
|
||||||
|
.leaflet-tile::selection {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||||
|
.leaflet-safari .leaflet-tile {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
}
|
||||||
|
/* hack that prevents hw layers "stretching" when loading new tiles */
|
||||||
|
.leaflet-safari .leaflet-tile-container {
|
||||||
|
width: 1600px;
|
||||||
|
height: 1600px;
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
|
||||||
|
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container .leaflet-overlay-pane svg {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-marker-pane img,
|
||||||
|
.leaflet-container .leaflet-shadow-pane img,
|
||||||
|
.leaflet-container .leaflet-tile-pane img,
|
||||||
|
.leaflet-container img.leaflet-image-layer,
|
||||||
|
.leaflet-container .leaflet-tile {
|
||||||
|
max-width: none !important;
|
||||||
|
max-height: none !important;
|
||||||
|
width: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container img.leaflet-tile {
|
||||||
|
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
|
||||||
|
mix-blend-mode: plus-lighter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: pan-x pan-y;
|
||||||
|
touch-action: pan-x pan-y;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag {
|
||||||
|
-ms-touch-action: pinch-zoom;
|
||||||
|
/* Fallback for FF which doesn't support pinch-zoom */
|
||||||
|
touch-action: none;
|
||||||
|
touch-action: pinch-zoom;
|
||||||
|
}
|
||||||
|
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
|
||||||
|
-ms-touch-action: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
z-index: 800;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-pane { z-index: 400; }
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 200; }
|
||||||
|
.leaflet-overlay-pane { z-index: 400; }
|
||||||
|
.leaflet-shadow-pane { z-index: 500; }
|
||||||
|
.leaflet-marker-pane { z-index: 600; }
|
||||||
|
.leaflet-tooltip-pane { z-index: 650; }
|
||||||
|
.leaflet-popup-pane { z-index: 700; }
|
||||||
|
|
||||||
|
.leaflet-map-pane canvas { z-index: 100; }
|
||||||
|
.leaflet-map-pane svg { z-index: 200; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 800;
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-animated {
|
||||||
|
-webkit-transform-origin: 0 0;
|
||||||
|
-ms-transform-origin: 0 0;
|
||||||
|
transform-origin: 0 0;
|
||||||
|
}
|
||||||
|
svg.leaflet-zoom-animated {
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-grab {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
.leaflet-crosshair,
|
||||||
|
.leaflet-crosshair .leaflet-interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-grab,
|
||||||
|
.leaflet-dragging .leaflet-grab .leaflet-interactive,
|
||||||
|
.leaflet-dragging .leaflet-marker-draggable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* marker & overlays interactivity */
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-pane > svg path,
|
||||||
|
.leaflet-tile-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-marker-icon.leaflet-interactive,
|
||||||
|
.leaflet-image-layer.leaflet-interactive,
|
||||||
|
.leaflet-pane > svg path.leaflet-interactive,
|
||||||
|
svg.leaflet-image-layer.leaflet-interactive path {
|
||||||
|
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover,
|
||||||
|
.leaflet-bar a:focus {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 2px;
|
||||||
|
border-top-right-radius: 2px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 2px;
|
||||||
|
border-bottom-right-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-scrollbar {
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default icon URLs */
|
||||||
|
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
|
||||||
|
background-image: url(images/marker-icon.png);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover,
|
||||||
|
.leaflet-control-attribution a:focus {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-attribution-flag {
|
||||||
|
display: inline !important;
|
||||||
|
vertical-align: baseline !important;
|
||||||
|
width: 1em;
|
||||||
|
height: 0.6669em;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
white-space: nowrap;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
text-shadow: 1px 1px #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 24px 13px 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-size: 13px;
|
||||||
|
font-size: 1.08333em;
|
||||||
|
min-height: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 17px 0;
|
||||||
|
margin: 1.3em 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: -1px;
|
||||||
|
margin-left: -20px;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
color: #333;
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
font: 16px/24px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover,
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:focus {
|
||||||
|
color: #585858;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
-ms-zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tooltip */
|
||||||
|
/* Base styles for the element that has a tooltip */
|
||||||
|
.leaflet-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: #fff;
|
||||||
|
border: 1px solid #fff;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #222;
|
||||||
|
white-space: nowrap;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-tooltip.leaflet-interactive {
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before,
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Directions */
|
||||||
|
|
||||||
|
.leaflet-tooltip-bottom {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top {
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before,
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-top:before {
|
||||||
|
bottom: 0;
|
||||||
|
margin-bottom: -12px;
|
||||||
|
border-top-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-bottom:before {
|
||||||
|
top: 0;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -6px;
|
||||||
|
border-bottom-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left {
|
||||||
|
margin-left: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right {
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before,
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
top: 50%;
|
||||||
|
margin-top: -6px;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-left:before {
|
||||||
|
right: 0;
|
||||||
|
margin-right: -12px;
|
||||||
|
border-left-color: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-tooltip-right:before {
|
||||||
|
left: 0;
|
||||||
|
margin-left: -12px;
|
||||||
|
border-right-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Printing */
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
/* Prevent printers from removing background-images of controls. */
|
||||||
|
.leaflet-control {
|
||||||
|
-webkit-print-color-adjust: exact;
|
||||||
|
print-color-adjust: exact;
|
||||||
|
}
|
||||||
|
}
|
||||||
164
scripts/fetch-schools.ts
Normal file
164
scripts/fetch-schools.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Fetches active AU school organisations from Airtable,
|
||||||
|
* geocodes their addresses via Nominatim, and writes
|
||||||
|
* an anonymous lat/lng JSON file for the homepage map.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* AIRTABLE_API_KEY=pat... AIRTABLE_BASE_ID=app... npx tsx scripts/fetch-schools.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { writeFileSync, mkdirSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
const AIRTABLE_API_KEY = process.env.AIRTABLE_API_KEY;
|
||||||
|
const AIRTABLE_BASE_ID = process.env.AIRTABLE_BASE_ID;
|
||||||
|
const TABLE_NAME = "Organisation";
|
||||||
|
|
||||||
|
if (!AIRTABLE_API_KEY || !AIRTABLE_BASE_ID) {
|
||||||
|
console.error("Missing AIRTABLE_API_KEY or AIRTABLE_BASE_ID env vars");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AirtableRecord {
|
||||||
|
id: string;
|
||||||
|
fields: {
|
||||||
|
"Google Formatted Address"?: string;
|
||||||
|
State?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AirtableResponse {
|
||||||
|
records: AirtableRecord[];
|
||||||
|
offset?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SchoolPin {
|
||||||
|
lat: number;
|
||||||
|
lng: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchAllRecords(): Promise<AirtableRecord[]> {
|
||||||
|
const allRecords: AirtableRecord[] = [];
|
||||||
|
let offset: string | undefined;
|
||||||
|
|
||||||
|
const filterFormula = "{Organisation Type}='Customer'";
|
||||||
|
const fields = ["Google Formatted Address", "State"];
|
||||||
|
|
||||||
|
do {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
filterByFormula: filterFormula,
|
||||||
|
});
|
||||||
|
fields.forEach((f) => params.append("fields[]", f));
|
||||||
|
if (offset) params.set("offset", offset);
|
||||||
|
|
||||||
|
const url = `https://api.airtable.com/v0/${AIRTABLE_BASE_ID}/${encodeURIComponent(TABLE_NAME)}?${params}`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { Authorization: `Bearer ${AIRTABLE_API_KEY}` },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
throw new Error(`Airtable API error ${res.status}: ${body}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: AirtableResponse = await res.json();
|
||||||
|
allRecords.push(...data.records);
|
||||||
|
offset = data.offset;
|
||||||
|
} while (offset);
|
||||||
|
|
||||||
|
return allRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
function simplifyAddress(address: string): string[] {
|
||||||
|
const variants: string[] = [];
|
||||||
|
|
||||||
|
// Strip unit/lot prefixes like "4506/793" → "793"
|
||||||
|
let simplified = address.replace(/\d+\/(\d+)/, "$1");
|
||||||
|
// Replace "&" intersections with just the first street
|
||||||
|
simplified = simplified.replace(/\s*&\s*[^,]+/, "");
|
||||||
|
if (simplified !== address) variants.push(simplified);
|
||||||
|
|
||||||
|
// Try just suburb + state + postcode (everything after the first comma)
|
||||||
|
const afterFirstComma = address.replace(/^[^,]+,\s*/, "");
|
||||||
|
if (afterFirstComma !== address) variants.push(afterFirstComma);
|
||||||
|
|
||||||
|
return variants;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function geocodeSingle(query: string): Promise<{ lat: number; lng: number } | null> {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query,
|
||||||
|
format: "json",
|
||||||
|
limit: "1",
|
||||||
|
countrycodes: "au",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
|
||||||
|
headers: { "User-Agent": "micromelon-website-build/1.0" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return null;
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.length) return null;
|
||||||
|
|
||||||
|
return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function geocode(address: string): Promise<{ lat: number; lng: number } | null> {
|
||||||
|
// Try the full address first
|
||||||
|
const result = await geocodeSingle(address);
|
||||||
|
if (result) return result;
|
||||||
|
|
||||||
|
// Try simplified variants
|
||||||
|
for (const variant of simplifyAddress(address)) {
|
||||||
|
await new Promise((r) => setTimeout(r, 1100));
|
||||||
|
const fallback = await geocodeSingle(variant);
|
||||||
|
if (fallback) return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log("Fetching records from Airtable...");
|
||||||
|
const records = await fetchAllRecords();
|
||||||
|
console.log(`Found ${records.length} active AU school records`);
|
||||||
|
|
||||||
|
const pins: SchoolPin[] = [];
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
for (const record of records) {
|
||||||
|
const address = record.fields["Google Formatted Address"];
|
||||||
|
if (!address) {
|
||||||
|
failed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate-limit: Nominatim asks for max 1 req/sec
|
||||||
|
await new Promise((r) => setTimeout(r, 1100));
|
||||||
|
|
||||||
|
const coords = await geocode(address);
|
||||||
|
if (coords) {
|
||||||
|
pins.push(coords);
|
||||||
|
process.stdout.write(` Geocoded ${pins.length}/${records.length}\r`);
|
||||||
|
} else {
|
||||||
|
console.warn(` Failed to geocode: ${address}`);
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nGeocoded ${pins.length} schools (${failed} failed)`);
|
||||||
|
|
||||||
|
const outDir = join(__dirname, "..", "src", "data");
|
||||||
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
const outPath = join(outDir, "schools.json");
|
||||||
|
writeFileSync(outPath, JSON.stringify(pins, null, 2) + "\n");
|
||||||
|
console.log(`Wrote ${outPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
63
scripts/seed-schools-json.mjs
Normal file
63
scripts/seed-schools-json.mjs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
/**
|
||||||
|
* One-time script: geocodes the existing KNOWN_SCHOOLS list to create
|
||||||
|
* an initial src/data/schools.json. Run once, then use fetch-schools.ts
|
||||||
|
* with Airtable for ongoing updates.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
||||||
|
import { join, dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// Parse the school names from schools.ts
|
||||||
|
const schoolsTs = readFileSync(join(__dirname, "..", "src", "data", "schools.ts"), "utf-8");
|
||||||
|
const match = schoolsTs.match(/\[([^\]]+)\]/s);
|
||||||
|
const schools = match
|
||||||
|
? match[1]
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim().replace(/^"|"$/g, "").replace(/^'|'$/g, ""))
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
console.log(`Found ${schools.length} schools to geocode`);
|
||||||
|
|
||||||
|
async function geocode(query) {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
q: query + ", Australia",
|
||||||
|
format: "json",
|
||||||
|
limit: "1",
|
||||||
|
countrycodes: "au",
|
||||||
|
});
|
||||||
|
|
||||||
|
const res = await fetch(`https://nominatim.openstreetmap.org/search?${params}`, {
|
||||||
|
headers: { "User-Agent": "micromelon-website-seed/1.0" },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return null;
|
||||||
|
const data = await res.json();
|
||||||
|
if (!data.length) return null;
|
||||||
|
return { lat: parseFloat(data[0].lat), lng: parseFloat(data[0].lon) };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const pins = [];
|
||||||
|
for (let i = 0; i < schools.length; i++) {
|
||||||
|
await new Promise((r) => setTimeout(r, 1100));
|
||||||
|
const coords = await geocode(schools[i]);
|
||||||
|
if (coords) {
|
||||||
|
pins.push(coords);
|
||||||
|
process.stdout.write(` ${i + 1}/${schools.length} — ${pins.length} geocoded\r`);
|
||||||
|
} else {
|
||||||
|
console.log(` MISS: ${schools[i]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nDone: ${pins.length} geocoded out of ${schools.length}`);
|
||||||
|
const outDir = join(__dirname, "..", "src", "data");
|
||||||
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
writeFileSync(join(outDir, "schools.json"), JSON.stringify(pins, null, 2) + "\n");
|
||||||
|
console.log("Wrote src/data/schools.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(console.error);
|
||||||
@@ -31,6 +31,9 @@ export default function RootLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="/leaflet.css" />
|
||||||
|
</head>
|
||||||
<body className={`${inter.variable} antialiased`}>
|
<body className={`${inter.variable} antialiased`}>
|
||||||
<Header />
|
<Header />
|
||||||
<main className="min-h-screen pt-16">{children}</main>
|
<main className="min-h-screen pt-16">{children}</main>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Button } from "@/components/ui/Button";
|
|||||||
import { SectionHeading } from "@/components/ui/SectionHeading";
|
import { SectionHeading } from "@/components/ui/SectionHeading";
|
||||||
import { LearningPathway } from "@/components/ui/LearningPathway";
|
import { LearningPathway } from "@/components/ui/LearningPathway";
|
||||||
import { partners } from "@/data/partners";
|
import { partners } from "@/data/partners";
|
||||||
import { KNOWN_SCHOOLS } from "@/data/schools";
|
import SchoolMapLoader from "@/components/SchoolMapLoader";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Micromelon Robotics",
|
title: "Micromelon Robotics",
|
||||||
@@ -152,27 +152,7 @@ export default function HomePage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Schools */}
|
{/* Schools */}
|
||||||
<section className="bg-muted py-16">
|
<SchoolMapLoader />
|
||||||
<Container>
|
|
||||||
<SectionHeading
|
|
||||||
title="Over 120 Schools Using Micromelon"
|
|
||||||
subtitle="From primary schools to universities across Australia."
|
|
||||||
/>
|
|
||||||
<div className="mx-auto max-w-4xl columns-2 gap-x-8 sm:columns-3 md:columns-4">
|
|
||||||
{KNOWN_SCHOOLS.slice(0, 50).map((school) => (
|
|
||||||
<p
|
|
||||||
key={school}
|
|
||||||
className="mb-1.5 break-inside-avoid text-sm text-foreground-light"
|
|
||||||
>
|
|
||||||
{school}
|
|
||||||
</p>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="mt-6 text-center text-sm font-medium text-foreground-light">
|
|
||||||
and many more...
|
|
||||||
</p>
|
|
||||||
</Container>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Partners */}
|
{/* Partners */}
|
||||||
<section className="bg-white py-16">
|
<section className="bg-white py-16">
|
||||||
|
|||||||
95
src/components/SchoolMap.tsx
Normal file
95
src/components/SchoolMap.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { MapContainer, TileLayer, CircleMarker, useMap } from "react-leaflet";
|
||||||
|
import schoolPins from "@/data/schools.json";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function MapSetup() {
|
||||||
|
const map = useMap();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Leaflet needs a kick after dynamic import to know its real size
|
||||||
|
setTimeout(() => map.invalidateSize(), 0);
|
||||||
|
|
||||||
|
// Center on Australia
|
||||||
|
map.setView([-28, 134], 3.5);
|
||||||
|
|
||||||
|
// Disable all interaction — static display only
|
||||||
|
map.dragging.disable();
|
||||||
|
map.touchZoom.disable();
|
||||||
|
map.doubleClickZoom.disable();
|
||||||
|
map.scrollWheelZoom.disable();
|
||||||
|
map.boxZoom.disable();
|
||||||
|
map.keyboard.disable();
|
||||||
|
if ((map as any).tap) (map as any).tap.disable();
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SchoolMap() {
|
||||||
|
return (
|
||||||
|
<div className="relative" style={{ height: 560, width: "100%" }}>
|
||||||
|
<MapContainer
|
||||||
|
center={[-25.5, 134]}
|
||||||
|
zoom={4}
|
||||||
|
zoomControl={false}
|
||||||
|
attributionControl={false}
|
||||||
|
style={{ height: "100%", width: "100%" }}
|
||||||
|
>
|
||||||
|
{/* Base map without labels */}
|
||||||
|
<TileLayer
|
||||||
|
url="https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png"
|
||||||
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> © <a href="https://carto.com/">CARTO</a>'
|
||||||
|
/>
|
||||||
|
{/* Gradient glow — concentric rings, dimmer further out */}
|
||||||
|
{[
|
||||||
|
{ radius: 22, opacity: 0.04 },
|
||||||
|
{ radius: 16, opacity: 0.08 },
|
||||||
|
{ radius: 10, opacity: 0.15 },
|
||||||
|
].map((ring) =>
|
||||||
|
schoolPins.map((pin, i) => (
|
||||||
|
<CircleMarker
|
||||||
|
key={`glow-${ring.radius}-${i}`}
|
||||||
|
center={[pin.lat, pin.lng]}
|
||||||
|
radius={ring.radius}
|
||||||
|
pathOptions={{
|
||||||
|
color: "transparent",
|
||||||
|
fillColor: "#F5A623",
|
||||||
|
fillOpacity: ring.opacity,
|
||||||
|
weight: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
{/* Solid dot */}
|
||||||
|
{schoolPins.map((pin, i) => (
|
||||||
|
<CircleMarker
|
||||||
|
key={i}
|
||||||
|
center={[pin.lat, pin.lng]}
|
||||||
|
radius={3}
|
||||||
|
pathOptions={{
|
||||||
|
color: "#d4900e",
|
||||||
|
fillColor: "#F5A623",
|
||||||
|
fillOpacity: 0.9,
|
||||||
|
weight: 1.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<MapSetup />
|
||||||
|
</MapContainer>
|
||||||
|
<div className="pointer-events-none absolute inset-0 z-[1000] flex items-start justify-center pt-8 sm:pt-12">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-4xl font-bold tracking-tight text-foreground drop-shadow-[0_1px_2px_rgba(255,255,255,0.8)] sm:text-5xl">
|
||||||
|
Schools Using Micromelon
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-lg font-semibold text-foreground-light drop-shadow-[0_1px_2px_rgba(255,255,255,0.8)] sm:text-xl">
|
||||||
|
120+ schools across Australia
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/components/SchoolMapLoader.tsx
Normal file
14
src/components/SchoolMapLoader.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
|
||||||
|
const SchoolMap = dynamic(() => import("@/components/SchoolMap"), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<div className="h-[400px] w-full animate-pulse bg-foreground/5 sm:h-[480px]" />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function SchoolMapLoader() {
|
||||||
|
return <SchoolMap />;
|
||||||
|
}
|
||||||
442
src/data/schools.json
Normal file
442
src/data/schools.json
Normal file
@@ -0,0 +1,442 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"lat": -37.7470993,
|
||||||
|
"lng": 144.9990095
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5240086,
|
||||||
|
"lng": 153.2137627
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -21.120363,
|
||||||
|
"lng": 144.2152618
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.6560398,
|
||||||
|
"lng": 145.0326854
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -23.3698363,
|
||||||
|
"lng": 150.5067508
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -21.1125705,
|
||||||
|
"lng": 149.1567627
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -28.092001,
|
||||||
|
"lng": 153.3922967
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.4241189,
|
||||||
|
"lng": 152.9093994
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -38.0355233,
|
||||||
|
"lng": 145.2592145
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -32.7720651,
|
||||||
|
"lng": 151.5996816
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -35.2882497,
|
||||||
|
"lng": 149.1394495
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.624897,
|
||||||
|
"lng": 152.9707622
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5557271,
|
||||||
|
"lng": 151.9440799
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.6734214,
|
||||||
|
"lng": 153.1422588
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.9483376,
|
||||||
|
"lng": 150.8552614
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.774088,
|
||||||
|
"lng": 153.1036408
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -36.1288805,
|
||||||
|
"lng": 146.861592
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.8688383,
|
||||||
|
"lng": 153.2983642
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.3118477,
|
||||||
|
"lng": 153.0029511
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.2976778,
|
||||||
|
"lng": 152.9650592
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.8767547,
|
||||||
|
"lng": 145.1368525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.7072284,
|
||||||
|
"lng": 153.1483804
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4714517,
|
||||||
|
"lng": 153.1511855
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4152112,
|
||||||
|
"lng": 152.9893319
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4575131,
|
||||||
|
"lng": 153.0252896
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.517615,
|
||||||
|
"lng": 152.9436013
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.6232019,
|
||||||
|
"lng": 152.9622034
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.7102598,
|
||||||
|
"lng": 153.1982203
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.8738021,
|
||||||
|
"lng": 151.205592
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -21.1399601,
|
||||||
|
"lng": 149.1847447
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.7229788,
|
||||||
|
"lng": 144.9901913
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.6387676,
|
||||||
|
"lng": 153.1294174
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.3238094,
|
||||||
|
"lng": 153.0825981
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -36.030406,
|
||||||
|
"lng": 146.9843354
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -12.6269041,
|
||||||
|
"lng": 141.8802825
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5555213,
|
||||||
|
"lng": 153.2531729
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -23.1178776,
|
||||||
|
"lng": 150.7278551
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -21.1166674,
|
||||||
|
"lng": 149.182293
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -25.2326385,
|
||||||
|
"lng": 152.2739552
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.7010073,
|
||||||
|
"lng": 144.8054491
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.2886947,
|
||||||
|
"lng": 152.9536783
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.724656,
|
||||||
|
"lng": 151.120942
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -41.6870276,
|
||||||
|
"lng": 147.0800587
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -32.0528106,
|
||||||
|
"lng": 150.8673349
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -16.9012244,
|
||||||
|
"lng": 145.7564813
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -22.0058932,
|
||||||
|
"lng": 148.0403261
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.2398578,
|
||||||
|
"lng": 153.0254814
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.660526,
|
||||||
|
"lng": 153.117401
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -23.3884898,
|
||||||
|
"lng": 150.5010833
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -21.101477,
|
||||||
|
"lng": 149.1733104
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.7574733,
|
||||||
|
"lng": 144.914184
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.8626452,
|
||||||
|
"lng": 151.2712213
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -24.4017852,
|
||||||
|
"lng": 150.5133571
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.1086456,
|
||||||
|
"lng": 152.9525619
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -19.2778499,
|
||||||
|
"lng": 146.7990098
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -28.2330978,
|
||||||
|
"lng": 153.5062211
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -41.3244809,
|
||||||
|
"lng": 148.2462093
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -28.2249897,
|
||||||
|
"lng": 152.0266401
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -34.1090269,
|
||||||
|
"lng": 150.7481889
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.584498,
|
||||||
|
"lng": 153.1228658
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -36.3907542,
|
||||||
|
"lng": 148.595996
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -31.1292591,
|
||||||
|
"lng": 150.9413209
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4582814,
|
||||||
|
"lng": 152.9804184
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.7325337,
|
||||||
|
"lng": 151.3012869
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.882939,
|
||||||
|
"lng": 151.1151587
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.9110974,
|
||||||
|
"lng": 145.0386427
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.7096197,
|
||||||
|
"lng": 153.0565813
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -36.7616888,
|
||||||
|
"lng": 144.271027
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -28.0874637,
|
||||||
|
"lng": 153.379925
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.3594608,
|
||||||
|
"lng": 153.0640429
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.8652775,
|
||||||
|
"lng": 145.1305158
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5050162,
|
||||||
|
"lng": 152.9845071
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4850834,
|
||||||
|
"lng": 153.0531124
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4601989,
|
||||||
|
"lng": 153.0184366
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.7462292,
|
||||||
|
"lng": 151.134731
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4526498,
|
||||||
|
"lng": 153.0639908
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.6133559,
|
||||||
|
"lng": 152.9730796
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -28.8622657,
|
||||||
|
"lng": 153.5302213
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.741528,
|
||||||
|
"lng": 151.059437
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4681298,
|
||||||
|
"lng": 152.9729962
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.2427895,
|
||||||
|
"lng": 153.0433373
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.6870455,
|
||||||
|
"lng": 153.1016224
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -28.0000721,
|
||||||
|
"lng": 153.3277205
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -25.297344,
|
||||||
|
"lng": 152.8154248
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4869062,
|
||||||
|
"lng": 152.9878806
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5414255,
|
||||||
|
"lng": 151.9539225
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -33.9006998,
|
||||||
|
"lng": 151.1320518
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -37.8540303,
|
||||||
|
"lng": 145.1193094
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.741302,
|
||||||
|
"lng": 153.1266712
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.5359734,
|
||||||
|
"lng": 151.8403736
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4609906,
|
||||||
|
"lng": 153.0338245
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5344463,
|
||||||
|
"lng": 153.1067123
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4383857,
|
||||||
|
"lng": 152.9853557
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -34.8937504,
|
||||||
|
"lng": 138.5944799
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.1865427,
|
||||||
|
"lng": 152.6796877
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5270447,
|
||||||
|
"lng": 153.2146544
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -26.543087,
|
||||||
|
"lng": 151.8338523
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.618754,
|
||||||
|
"lng": 153.0920355
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -23.1306451,
|
||||||
|
"lng": 150.7436057
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4843986,
|
||||||
|
"lng": 153.0504723
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4248283,
|
||||||
|
"lng": 153.0562791
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5459026,
|
||||||
|
"lng": 152.9875492
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -17.004629,
|
||||||
|
"lng": 145.7416381
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4484886,
|
||||||
|
"lng": 153.0140922
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -32.0062232,
|
||||||
|
"lng": 116.038894
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.4945588,
|
||||||
|
"lng": 153.0279224
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -23.1350531,
|
||||||
|
"lng": 150.7370891
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5061536,
|
||||||
|
"lng": 152.9678737
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -27.5900938,
|
||||||
|
"lng": 152.7445426
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lat": -35.247,
|
||||||
|
"lng": 149.083
|
||||||
|
}
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user