Skip to content

Commit 8fc87d0

Browse files
committed
Replace search bar with collapsible bubble + add backend geocoding API
UI Changes (route_dog inspired): - Replace always-visible search bar with collapsible bubble - Circular button (closed state) with search icon, matches MapLibre controls - Expands to search input + results on click - Collapses after selection or clicking outside - Cleaner, less cluttered map interface Backend API (route_dog pattern): - Create Cloudflare Worker API for geocoding (/v1/geocode) - Use Photon API on backend (international coverage, unlike US Census) - Frontend calls our backend instead of Photon directly - Enables caching and rate limiting control Bug Fixes: - Fix repeated Photon API calls by wrapping search function in useCallback - Add TypeScript definitions for Vite environment variables
1 parent 0875891 commit 8fc87d0

File tree

11 files changed

+573
-281
lines changed

11 files changed

+573
-281
lines changed

api/package.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "farese-api",
3+
"version": "1.0.0",
4+
"description": "Farese geocoding API",
5+
"main": "src/index.ts",
6+
"scripts": {
7+
"dev": "wrangler dev",
8+
"deploy": "wrangler deploy",
9+
"deploy:dev": "wrangler deploy --env dev",
10+
"deploy:prod": "wrangler deploy --env production"
11+
},
12+
"devDependencies": {
13+
"@cloudflare/workers-types": "^4.20250110.0",
14+
"wrangler": "^4.59.2"
15+
}
16+
}

api/src/index.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/**
2+
* Farese Geocoding API - Cloudflare Worker
3+
*
4+
* Geocoding service using Photon API for international coverage.
5+
*/
6+
7+
export interface Env {
8+
ENVIRONMENT: string;
9+
}
10+
11+
interface GeocodingResult {
12+
name: string;
13+
coordinates: [number, number];
14+
city?: string;
15+
state?: string;
16+
country?: string;
17+
}
18+
19+
export default {
20+
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
21+
const url = new URL(request.url);
22+
const { pathname, method } = { pathname: url.pathname, method: request.method };
23+
24+
// CORS headers
25+
const corsHeaders = {
26+
'Access-Control-Allow-Origin': '*',
27+
'Access-Control-Allow-Methods': 'GET, OPTIONS',
28+
'Access-Control-Allow-Headers': 'Content-Type',
29+
'Content-Type': 'application/json',
30+
};
31+
32+
// Handle CORS preflight
33+
if (method === 'OPTIONS') {
34+
return new Response(null, { status: 204, headers: corsHeaders });
35+
}
36+
37+
try {
38+
// Health check endpoints
39+
if (pathname === '/' || pathname === '/health') {
40+
return jsonResponse({
41+
status: 'healthy',
42+
message: 'Farese Geocoding API is running'
43+
}, corsHeaders);
44+
}
45+
46+
// GET /v1/geocode?q=Brisbane - Geocode place name
47+
if (pathname === '/v1/geocode' && method === 'GET') {
48+
return await handleGeocode(request, corsHeaders);
49+
}
50+
51+
// Not found
52+
return jsonResponse({ error: 'Not found' }, corsHeaders, 404);
53+
} catch (error) {
54+
const message = error instanceof Error ? error.message : 'Unknown error';
55+
return jsonResponse({ error: message }, corsHeaders, 500);
56+
}
57+
},
58+
};
59+
60+
/**
61+
* Handle GET /v1/geocode - Geocode place name using Photon API
62+
*/
63+
async function handleGeocode(
64+
request: Request,
65+
corsHeaders: Record<string, string>
66+
): Promise<Response> {
67+
const url = new URL(request.url);
68+
const query = url.searchParams.get('q');
69+
const limit = parseInt(url.searchParams.get('limit') || '5', 10);
70+
71+
if (!query) {
72+
return jsonResponse({ error: 'Query parameter "q" is required' }, corsHeaders, 400);
73+
}
74+
75+
if (query.length < 3) {
76+
return jsonResponse({ error: 'Query must be at least 3 characters' }, corsHeaders, 400);
77+
}
78+
79+
try {
80+
const results = await geocodeWithPhoton(query, limit);
81+
return jsonResponse({ results }, corsHeaders);
82+
} catch (error) {
83+
const message = error instanceof Error ? error.message : 'Geocoding failed';
84+
return jsonResponse({ error: message }, corsHeaders, 500);
85+
}
86+
}
87+
88+
/**
89+
* Geocode using Photon API (OpenStreetMap-based, international coverage)
90+
*/
91+
async function geocodeWithPhoton(query: string, limit: number): Promise<GeocodingResult[]> {
92+
const url = `https://photon.komoot.io/api/?q=${encodeURIComponent(query)}&limit=${limit}`;
93+
94+
const response = await fetch(url, {
95+
headers: {
96+
'User-Agent': 'Farese.com Church Directory'
97+
}
98+
});
99+
100+
if (!response.ok) {
101+
throw new Error(`Photon API error: ${response.status}`);
102+
}
103+
104+
const data = await response.json() as {
105+
features?: Array<{
106+
geometry: {
107+
coordinates: [number, number];
108+
};
109+
properties: {
110+
name?: string;
111+
city?: string;
112+
state?: string;
113+
country?: string;
114+
};
115+
}>;
116+
};
117+
118+
if (!data.features || data.features.length === 0) {
119+
return [];
120+
}
121+
122+
return data.features.map(feature => ({
123+
name: feature.properties.name || 'Unknown',
124+
coordinates: feature.geometry.coordinates,
125+
city: feature.properties.city,
126+
state: feature.properties.state,
127+
country: feature.properties.country,
128+
}));
129+
}
130+
131+
/**
132+
* Helper to create JSON responses
133+
*/
134+
function jsonResponse(data: any, headers: Record<string, string>, status: number = 200): Response {
135+
return new Response(JSON.stringify(data), {
136+
status,
137+
headers,
138+
});
139+
}

api/tsconfig.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2021",
4+
"lib": ["ES2021"],
5+
"module": "ES2022",
6+
"moduleResolution": "bundler",
7+
"types": ["@cloudflare/workers-types"],
8+
"resolveJsonModule": true,
9+
"allowJs": true,
10+
"checkJs": false,
11+
"strict": true,
12+
"esModuleInterop": true,
13+
"skipLibCheck": true,
14+
"forceConsistentCasingInFileNames": true,
15+
"isolatedModules": true,
16+
"noEmit": true
17+
},
18+
"include": ["src/**/*"]
19+
}

api/wrangler.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
name = "farese-api"
2+
main = "src/index.ts"
3+
compatibility_date = "2024-01-01"
4+
5+
[env.dev]
6+
name = "farese-api-dev"
7+
route = { pattern = "api-dev.farese.com/*", zone_name = "farese.com" }
8+
9+
[env.production]
10+
name = "farese-api"
11+
route = { pattern = "api.farese.com/*", zone_name = "farese.com" }

0 commit comments

Comments
 (0)