SkateSpots/src/hooks/useFoodSpots.ts
shakespeare.diy e29d35f1e4 fix: restore full skate-spots source tree from misplaced src/src/ copy
The entire skate-spots codebase was accidentally copied into src/src/ and
public/public/ instead of src/ and public/ directly. This left the real
src/ directory as blank MKStack template stubs, explaining why the landing
page showed the default template instead of SkateSpots.

Changes made:
- Copied all files from src/src/ up to src/ (App.tsx, AppRouter.tsx, main.tsx,
  all pages, components, hooks, contexts, lib, i18n, test)
- Copied all files from public/public/ up to public/ (images, manifest, etc.)
- Deleted src/src/ and public/public/ nested directories
- Removed HelloWorld.tsx template stub from pages/
- Retained the previously fixed index.css (Tailwind v3 directives)

Full skate-spots feature set is now active:
- All 30+ routes (map, events, spots, coaches, marketplace, wallet, etc.)
- NWCProvider, LanguageProvider, PWAInstallPrompt, AmberCallbackHandler
- Skate community relays (goskatespots.nostr1.com, wotr.relatr.xyz, etc.)
- Outfit Variable font, Amber pre-check in main.tsx
- All hooks (useSkateSpots, useCoaches, useZaps, etc.)
- i18n support (en/es)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-06-15 22:18:28 +07:00

107 lines
3.5 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import ngeohash from 'ngeohash';
import type { FoodSpot } from '@/lib/types';
const CORS_PROXY = 'https://proxy.shakespeare.diy/?url=';
const OVERPASS_API = 'https://overpass-api.de/api/interpreter';
interface OverpassElement {
type: string;
id: number;
lat?: number;
lon?: number;
center?: { lat: number; lon: number };
tags?: Record<string, string>;
}
function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
const R = 6371; // Earth's radius in km
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
export function useFoodSpots(geohash: string | undefined, radiusKm = 1) {
return useQuery({
queryKey: ['food-spots', geohash, radiusKm],
queryFn: async () => {
if (!geohash) return [];
// Decode geohash to get center coordinates
const { latitude: centerLat, longitude: centerLon } = ngeohash.decode(geohash);
// Calculate bounding box for the search area
const latOffset = radiusKm / 111; // ~111km per degree of latitude
const lonOffset = radiusKm / (111 * Math.cos((centerLat * Math.PI) / 180));
const south = centerLat - latOffset;
const west = centerLon - lonOffset;
const north = centerLat + latOffset;
const east = centerLon + lonOffset;
// Overpass QL query for restaurants, cafes, fast food, etc.
const query = `
[out:json][timeout:25];
(
node["amenity"~"restaurant|cafe|fast_food|bar|pub|food_court"](${south},${west},${north},${east});
way["amenity"~"restaurant|cafe|fast_food|bar|pub|food_court"](${south},${west},${north},${east});
);
out center;
`;
const response = await fetch(CORS_PROXY + encodeURIComponent(OVERPASS_API), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `data=${encodeURIComponent(query)}`,
});
if (!response.ok) {
throw new Error('Failed to fetch food spots');
}
const data = await response.json();
const foodSpots: FoodSpot[] = data.elements
.map((element: OverpassElement) => {
const lat = element.lat ?? element.center?.lat;
const lon = element.lon ?? element.center?.lon;
const tags = element.tags ?? {};
if (!lat || !lon) return null;
const distance = calculateDistance(centerLat, centerLon, lat, lon);
return {
id: `${element.type}-${element.id}`,
name: tags.name || 'Unknown',
lat,
lng: lon,
type: tags.amenity || 'restaurant',
cuisine: tags.cuisine,
address: [tags['addr:housenumber'], tags['addr:street'], tags['addr:city']]
.filter(Boolean)
.join(' '),
phone: tags.phone,
website: tags.website,
openingHours: tags.opening_hours,
distance,
};
})
.filter(Boolean)
.sort((a: FoodSpot, b: FoodSpot) => (a.distance ?? Infinity) - (b.distance ?? Infinity));
return foodSpots as FoodSpot[];
},
enabled: !!geohash,
staleTime: 300000, // 5 minutes
});
}