Skip to content

Commit 23e48c5

Browse files
committed
feat(website): add MapLibre playground, brand assets (logo/favicon), nav; integrate map CSS
1 parent 064ed2d commit 23e48c5

5 files changed

Lines changed: 131 additions & 2 deletions

File tree

app/layout.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import './globals.css'
2+
import 'maplibre-gl/dist/maplibre-gl.css'
23

34
export const metadata = {
45
title: 'Bharat Address',
@@ -8,12 +9,19 @@ export const metadata = {
89
export default function RootLayout({ children }: { children: React.ReactNode }) {
910
return (
1011
<html lang="en">
12+
<head>
13+
<link rel="icon" href="/favicon.svg" />
14+
</head>
1115
<body>
1216
<header>
13-
<a href="/" style={{ fontWeight: 700, fontSize: 18 }}>Bharat Address</a>
17+
<a href="/" style={{ fontWeight: 700, fontSize: 18, display: 'inline-flex', alignItems: 'center', gap: 8 }}>
18+
<img src="/logo.svg" alt="Bharat Address" width={24} height={24} />
19+
Bharat Address
20+
</a>
1421
<nav className="nav" style={{ float: 'right' }}>
1522
<a href="/docs/">Docs</a>
1623
<a href="/developers/">Developers</a>
24+
<a href="/playground/">Playground</a>
1725
<a href="/api/">API</a>
1826
<a href="https://github.com/BharatAddress" target="_blank" rel="noreferrer">GitHub</a>
1927
</nav>

app/playground/page.tsx

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"use client";
2+
3+
import { useEffect, useRef, useState } from 'react';
4+
import type { Map as MapLibreMap } from 'maplibre-gl';
5+
6+
export default function PlaygroundPage() {
7+
const mapRef = useRef<MapLibreMap | null>(null);
8+
const mapEl = useRef<HTMLDivElement | null>(null);
9+
const [baseUrl, setBaseUrl] = useState<string>(
10+
process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:8000'
11+
);
12+
const [pin, setPin] = useState<string>('');
13+
const [city, setCity] = useState<string>('');
14+
const [limit, setLimit] = useState<number>(200);
15+
const [loading, setLoading] = useState(false);
16+
const [error, setError] = useState<string | null>(null);
17+
18+
useEffect(() => {
19+
let canceled = false;
20+
(async () => {
21+
const { Map } = await import('maplibre-gl');
22+
if (canceled) return;
23+
const map = new Map({
24+
container: mapEl.current as HTMLDivElement,
25+
style: 'https://demotiles.maplibre.org/style.json',
26+
center: [78.9629, 20.5937], // India
27+
zoom: 4,
28+
attributionControl: true,
29+
});
30+
mapRef.current = map;
31+
})();
32+
return () => {
33+
canceled = true;
34+
mapRef.current?.remove();
35+
mapRef.current = null;
36+
};
37+
}, []);
38+
39+
const load = async () => {
40+
setLoading(true);
41+
setError(null);
42+
try {
43+
const url = new URL('/collections/addresses/items', baseUrl);
44+
url.searchParams.set('limit', String(limit));
45+
if (pin) url.searchParams.set('pin', pin);
46+
if (city) url.searchParams.set('city', city);
47+
const r = await fetch(url.toString());
48+
const gj = await r.json();
49+
if (!r.ok) throw new Error(gj?.error || `HTTP ${r.status}`);
50+
const map = mapRef.current;
51+
if (!map) return;
52+
const srcId = 'addresses';
53+
const layerId = 'addresses-layer';
54+
55+
if (map.getLayer(layerId)) map.removeLayer(layerId);
56+
if (map.getSource(srcId)) (map.getSource(srcId) as any).setData(gj);
57+
else map.addSource(srcId, { type: 'geojson', data: gj } as any);
58+
59+
map.addLayer({
60+
id: layerId,
61+
type: 'circle',
62+
source: srcId,
63+
paint: {
64+
'circle-radius': 5,
65+
'circle-color': '#d61f69',
66+
'circle-stroke-width': 1,
67+
'circle-stroke-color': '#ffffff',
68+
},
69+
} as any);
70+
71+
// Fit to data
72+
if (Array.isArray(gj?.features) && gj.features.length) {
73+
const bounds = new (await import('maplibre-gl')).LngLatBounds();
74+
for (const f of gj.features) {
75+
if (f?.geometry?.type === 'Point') {
76+
const [x, y] = f.geometry.coordinates;
77+
bounds.extend([x, y]);
78+
}
79+
}
80+
if (!bounds.isEmpty()) {
81+
map.fitBounds(bounds, { padding: 40, duration: 500 });
82+
}
83+
}
84+
} catch (e: any) {
85+
setError(e.message || String(e));
86+
} finally {
87+
setLoading(false);
88+
}
89+
};
90+
91+
return (
92+
<section>
93+
<h1>Playground</h1>
94+
<p>Visualize API items on a map using MapLibre GL.</p>
95+
96+
<div style={{ display: 'grid', gap: 8, maxWidth: 960, gridTemplateColumns: '1fr 1fr 1fr 120px' }}>
97+
<input placeholder="API Base (https://api.example.org)" value={baseUrl} onChange={(e) => setBaseUrl(e.target.value)} />
98+
<input placeholder="Filter PIN (e.g., 560008)" value={pin} onChange={(e) => setPin(e.target.value)} />
99+
<input placeholder="Filter City (e.g., Bengaluru)" value={city} onChange={(e) => setCity(e.target.value)} />
100+
<input type="number" min={1} max={10000} value={limit} onChange={(e) => setLimit(parseInt(e.target.value || '100', 10))} />
101+
</div>
102+
103+
<div style={{ marginTop: 8 }}>
104+
<button className="btn" disabled={loading} onClick={load}>{loading ? 'Loading…' : 'Load on Map'}</button>
105+
{error && <span style={{ color: 'crimson', marginLeft: 12 }}>Error: {error}</span>}
106+
</div>
107+
108+
<div ref={mapEl} style={{ height: 540, marginTop: 16, borderRadius: 8, overflow: 'hidden', border: '1px solid #eee' }} />
109+
</section>
110+
);
111+
}
112+

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
"dependencies": {
1212
"next": "^15.0.0",
1313
"react": "^18.3.1",
14-
"react-dom": "^18.3.1"
14+
"react-dom": "^18.3.1",
15+
"maplibre-gl": "^3.6.1"
1516
},
1617
"devDependencies": {
1718
"typescript": "^5.5.4",

public/favicon.svg

Lines changed: 4 additions & 0 deletions
Loading

public/logo.svg

Lines changed: 4 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)