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>
107 lines
3.5 KiB
TypeScript
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
|
|
});
|
|
}
|