Skip to content

Commit 587f2fb

Browse files
committed
feat(website): clustering + bbox controls; icon generation via pwa-asset-generator; manifest and head links; API presets
1 parent 23e48c5 commit 587f2fb

6 files changed

Lines changed: 113 additions & 20 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ jobs:
2323
- name: Install and build Next.js static site
2424
run: |
2525
npm ci || npm install
26+
npm run assets || npx pwa-asset-generator public/logo.svg public --favicon true --mstile true --manifest true --opaque false --background '#ffffff' --path /
2627
npm run build:static
2728
2829
- name: Checkout docs-site repo
@@ -57,4 +58,3 @@ jobs:
5758
publish_dir: ./out
5859
force_orphan: false
5960
keep_files: false
60-

app/api/page.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
import { useState } from 'react';
44

55
export default function ApiPage() {
6-
const [baseUrl, setBaseUrl] = useState<string>(
7-
process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:8000'
8-
);
6+
const envApi = process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:8000';
7+
const [baseUrl, setBaseUrl] = useState<string>(envApi);
98
const [query, setQuery] = useState<string>('/collections/addresses/items?limit=5');
109
const [resp, setResp] = useState<any>(null);
1110
const [loading, setLoading] = useState(false);
@@ -52,10 +51,16 @@ export default function ApiPage() {
5251
placeholder="/collections/addresses/items?limit=5"
5352
/>
5453
</label>
55-
<div>
54+
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
5655
<button className="btn" onClick={run} disabled={loading}>
5756
{loading ? 'Fetching…' : 'Fetch'}
5857
</button>
58+
<button className="btn outline" onClick={() => setBaseUrl(envApi)} title="Use configured live API base">
59+
Use Live API
60+
</button>
61+
<button className="btn outline" onClick={() => setBaseUrl('http://localhost:8000')} title="Use local dev base">
62+
Use Local
63+
</button>
5964
</div>
6065
</div>
6166

@@ -93,4 +98,3 @@ data = r.json()`}
9398
</section>
9499
);
95100
}
96-

app/layout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
1010
return (
1111
<html lang="en">
1212
<head>
13-
<link rel="icon" href="/favicon.svg" />
13+
<link rel="icon" href="/favicon.ico" />
14+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
15+
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
16+
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
17+
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
18+
<link rel="manifest" href="/site.webmanifest" />
1419
</head>
1520
<body>
1621
<header>

app/playground/page.tsx

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import type { Map as MapLibreMap } from 'maplibre-gl';
66
export default function PlaygroundPage() {
77
const mapRef = useRef<MapLibreMap | null>(null);
88
const mapEl = useRef<HTMLDivElement | null>(null);
9-
const [baseUrl, setBaseUrl] = useState<string>(
10-
process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:8000'
11-
);
9+
const envApi = process.env.NEXT_PUBLIC_API_BASE || 'http://localhost:8000';
10+
const [baseUrl, setBaseUrl] = useState<string>(envApi);
1211
const [pin, setPin] = useState<string>('');
1312
const [city, setCity] = useState<string>('');
1413
const [limit, setLimit] = useState<number>(200);
1514
const [loading, setLoading] = useState(false);
1615
const [error, setError] = useState<string | null>(null);
16+
const [useBbox, setUseBbox] = useState<boolean>(false);
17+
const [cluster, setCluster] = useState<boolean>(true);
1718

1819
useEffect(() => {
1920
let canceled = false;
@@ -44,22 +45,81 @@ export default function PlaygroundPage() {
4445
url.searchParams.set('limit', String(limit));
4546
if (pin) url.searchParams.set('pin', pin);
4647
if (city) url.searchParams.set('city', city);
48+
if (useBbox && mapRef.current) {
49+
const b = mapRef.current.getBounds();
50+
const bbox = `${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}`;
51+
url.searchParams.set('bbox', bbox);
52+
}
4753
const r = await fetch(url.toString());
4854
const gj = await r.json();
4955
if (!r.ok) throw new Error(gj?.error || `HTTP ${r.status}`);
5056
const map = mapRef.current;
5157
if (!map) return;
5258
const srcId = 'addresses';
53-
const layerId = 'addresses-layer';
59+
const pointLayerId = 'addresses-points';
60+
const clusterLayerId = 'addresses-clusters';
61+
const clusterCountId = 'addresses-cluster-count';
62+
63+
// Clean existing layers/sources
64+
for (const id of [clusterCountId, clusterLayerId, pointLayerId]) {
65+
if (map.getLayer(id)) map.removeLayer(id);
66+
}
67+
if (map.getSource(srcId)) map.removeSource(srcId);
5468

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);
69+
// Add source with optional clustering
70+
const sourceOpts: any = { type: 'geojson', data: gj };
71+
if (cluster) {
72+
sourceOpts.cluster = true;
73+
sourceOpts.clusterMaxZoom = 14;
74+
sourceOpts.clusterRadius = 50;
75+
}
76+
map.addSource(srcId, sourceOpts);
77+
78+
if (cluster) {
79+
map.addLayer({
80+
id: clusterLayerId,
81+
type: 'circle',
82+
source: srcId,
83+
filter: ['has', 'point_count'],
84+
paint: {
85+
'circle-color': [
86+
'step',
87+
['get', 'point_count'],
88+
'#99c6f3',
89+
10,
90+
'#66a6e8',
91+
50,
92+
'#2f7dd1',
93+
],
94+
'circle-radius': [
95+
'step',
96+
['get', 'point_count'],
97+
12,
98+
10,
99+
16,
100+
50,
101+
22,
102+
],
103+
},
104+
} as any);
105+
map.addLayer({
106+
id: clusterCountId,
107+
type: 'symbol',
108+
source: srcId,
109+
filter: ['has', 'point_count'],
110+
layout: {
111+
'text-field': ['get', 'point_count_abbreviated'],
112+
'text-size': 12,
113+
},
114+
paint: { 'text-color': '#fff' },
115+
} as any);
116+
}
58117

59118
map.addLayer({
60-
id: layerId,
119+
id: pointLayerId,
61120
type: 'circle',
62121
source: srcId,
122+
filter: cluster ? ['!', ['has', 'point_count']] : undefined,
63123
paint: {
64124
'circle-radius': 5,
65125
'circle-color': '#d61f69',
@@ -100,13 +160,16 @@ export default function PlaygroundPage() {
100160
<input type="number" min={1} max={10000} value={limit} onChange={(e) => setLimit(parseInt(e.target.value || '100', 10))} />
101161
</div>
102162

103-
<div style={{ marginTop: 8 }}>
163+
<div style={{ marginTop: 8, display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
164+
<label><input type="checkbox" checked={useBbox} onChange={(e) => setUseBbox(e.target.checked)} /> Use map view as bbox</label>
165+
<label><input type="checkbox" checked={cluster} onChange={(e) => setCluster(e.target.checked)} /> Cluster</label>
104166
<button className="btn" disabled={loading} onClick={load}>{loading ? 'Loading…' : 'Load on Map'}</button>
105-
{error && <span style={{ color: 'crimson', marginLeft: 12 }}>Error: {error}</span>}
167+
<button className="btn outline" onClick={() => setBaseUrl(envApi)}>Use Live API</button>
168+
<button className="btn outline" onClick={() => setBaseUrl('http://localhost:8000')}>Use Local</button>
169+
{error && <span style={{ color: 'crimson' }}>Error: {error}</span>}
106170
</div>
107171

108172
<div ref={mapEl} style={{ height: 540, marginTop: 16, borderRadius: 8, overflow: 'hidden', border: '1px solid #eee' }} />
109173
</section>
110174
);
111175
}
112-

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
"dev": "next dev",
77
"build": "next build",
88
"start": "next start",
9-
"build:static": "next build"
9+
"build:static": "next build",
10+
"assets": "pwa-asset-generator public/logo.svg public --favicon true --mstile true --manifest true --opaque false --background '#ffffff' --path /"
1011
},
1112
"dependencies": {
1213
"next": "^15.0.0",
@@ -18,6 +19,7 @@
1819
"typescript": "^5.5.4",
1920
"@types/node": "^20.14.11",
2021
"@types/react": "^18.3.3",
21-
"@types/react-dom": "^18.3.0"
22+
"@types/react-dom": "^18.3.0",
23+
"pwa-asset-generator": "^6.3.1"
2224
}
2325
}

public/site.webmanifest

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "Bharat Address",
3+
"short_name": "BharatAddr",
4+
"icons": [
5+
{
6+
"src": "/android-chrome-192x192.png",
7+
"sizes": "192x192",
8+
"type": "image/png"
9+
},
10+
{
11+
"src": "/android-chrome-512x512.png",
12+
"sizes": "512x512",
13+
"type": "image/png"
14+
}
15+
],
16+
"theme_color": "#0366d6",
17+
"background_color": "#ffffff",
18+
"display": "standalone"
19+
}

0 commit comments

Comments
 (0)