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>
139 lines
3.9 KiB
TypeScript
139 lines
3.9 KiB
TypeScript
import { useQuery } from '@tanstack/react-query';
|
|
|
|
export interface BTCMapMerchant {
|
|
id: string;
|
|
name: string;
|
|
lat: number;
|
|
lon: number;
|
|
tags: {
|
|
name?: string;
|
|
'payment:lightning'?: string;
|
|
'payment:onchain'?: string;
|
|
'payment:lightning_contactless'?: string;
|
|
amenity?: string;
|
|
shop?: string;
|
|
cuisine?: string;
|
|
opening_hours?: string;
|
|
website?: string;
|
|
phone?: string;
|
|
};
|
|
osm_json?: {
|
|
type: string;
|
|
id: number;
|
|
};
|
|
}
|
|
|
|
interface BTCMapResponse {
|
|
type: string;
|
|
features: Array<{
|
|
type: string;
|
|
geometry: {
|
|
type: string;
|
|
coordinates: [number, number]; // [lon, lat]
|
|
};
|
|
properties: {
|
|
id: string;
|
|
name?: string;
|
|
'payment:lightning'?: string;
|
|
'payment:onchain'?: string;
|
|
[key: string]: string | undefined;
|
|
};
|
|
}>;
|
|
}
|
|
|
|
/**
|
|
* Fetch Bitcoin-accepting merchants from BTCMap API
|
|
* Uses bounding box to get merchants in the visible map area
|
|
*/
|
|
export function useBTCMap(bounds?: {
|
|
north: number;
|
|
south: number;
|
|
east: number;
|
|
west: number;
|
|
}, enabled: boolean = false) {
|
|
return useQuery({
|
|
queryKey: ['btcmap', bounds?.north, bounds?.south, bounds?.east, bounds?.west],
|
|
queryFn: async ({ signal }): Promise<BTCMapMerchant[]> => {
|
|
if (!bounds) return [];
|
|
|
|
// BTCMap API endpoint - get elements in bounding box
|
|
// Using their public API
|
|
const url = `https://api.btcmap.org/v2/elements?updated_since=2020-01-01&bounds=${bounds.south},${bounds.west},${bounds.north},${bounds.east}`;
|
|
|
|
const response = await fetch(url, {
|
|
signal,
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`BTCMap API error: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Transform the response to our format
|
|
const merchants: BTCMapMerchant[] = data
|
|
.filter((item: { 'osm_json': { lat?: number; lon?: number }; deleted_at?: string }) =>
|
|
item.osm_json?.lat && item.osm_json?.lon && !item.deleted_at
|
|
)
|
|
.map((item: {
|
|
id: string;
|
|
'osm_json': {
|
|
lat: number;
|
|
lon: number;
|
|
tags?: Record<string, string>;
|
|
type?: string;
|
|
id?: number;
|
|
}
|
|
}) => ({
|
|
id: item.id,
|
|
name: item.osm_json.tags?.name || 'Bitcoin Merchant',
|
|
lat: item.osm_json.lat,
|
|
lon: item.osm_json.lon,
|
|
tags: item.osm_json.tags || {},
|
|
osm_json: {
|
|
type: item.osm_json.type || 'node',
|
|
id: item.osm_json.id || 0,
|
|
}
|
|
}));
|
|
|
|
return merchants;
|
|
},
|
|
enabled: enabled && !!bounds,
|
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
gcTime: 30 * 60 * 1000, // 30 minutes
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a human-readable category for the merchant
|
|
*/
|
|
export function getMerchantCategory(merchant: BTCMapMerchant): string {
|
|
const { tags } = merchant;
|
|
|
|
if (tags.amenity === 'cafe' || tags.amenity === 'coffee') return '☕ Café';
|
|
if (tags.amenity === 'restaurant') return '🍽️ Restaurant';
|
|
if (tags.amenity === 'bar' || tags.amenity === 'pub') return '🍺 Bar';
|
|
if (tags.amenity === 'fast_food') return '🍔 Fast Food';
|
|
if (tags.shop === 'convenience') return '🏪 Convenience';
|
|
if (tags.shop === 'supermarket') return '🛒 Supermarket';
|
|
if (tags.shop === 'clothes') return '👕 Clothing';
|
|
if (tags.shop === 'sports') return '⚽ Sports';
|
|
if (tags.shop === 'electronics') return '📱 Electronics';
|
|
if (tags.shop) return '🏬 Shop';
|
|
if (tags.amenity === 'atm') return '🏧 ATM';
|
|
if (tags.amenity === 'bank') return '🏦 Bank';
|
|
|
|
return '₿ Bitcoin Accepted';
|
|
}
|
|
|
|
/**
|
|
* Check if merchant accepts Lightning
|
|
*/
|
|
export function acceptsLightning(merchant: BTCMapMerchant): boolean {
|
|
const { tags } = merchant;
|
|
return tags['payment:lightning'] === 'yes' ||
|
|
tags['payment:lightning_contactless'] === 'yes';
|
|
}
|