'use client' import { useState, useEffect, useRef, useCallback } from 'react' import { useAuth } from '@/lib/AuthContext' import { Crown, Lock, Search, MapPin, Navigation, Bookmark, X, Loader2 } from 'lucide-react' import dynamic from 'next/dynamic' // Leaflet must be loaded client-side only (no SSR) const MapContainer = dynamic(() => import('react-leaflet').then(m => m.MapContainer), { ssr: false }) const TileLayer = dynamic(() => import('react-leaflet').then(m => m.TileLayer), { ssr: false }) const Marker = dynamic(() => import('react-leaflet').then(m => m.Marker), { ssr: false }) const Popup = dynamic(() => import('react-leaflet').then(m => m.Popup), { ssr: false }) interface Place { id: string lat: number lon: number name: string type: 'mosque' | 'restaurant' address?: string } interface MapCenter { lat: number lon: number } function UpgradeCTA() { return (

Halal Monitor

Find mosques and halal restaurants anywhere in the world. Available to Premium members.

Upgrade Now
) } function LoginGate() { return (

Halal Monitor

Sign in to access the global halal map — mosques, prayer spaces, and halal restaurants near you.

Sign In
) } export default function HalalMonitorPage() { const { token, user, loading: authLoading } = useAuth() const [places, setPlaces] = useState([]) const [center, setCenter] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [citySearch, setCitySearch] = useState('') const [bookmarked, setBookmarked] = useState>(new Set()) const [leafletLoaded, setLeafletLoaded] = useState(false) const mapRef = useRef(null) // Load Leaflet CSS useEffect(() => { if (typeof window === 'undefined') return const link = document.createElement('link') link.rel = 'stylesheet' link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' document.head.appendChild(link) setLeafletLoaded(true) return () => { document.head.removeChild(link) } }, []) // Load existing bookmarks useEffect(() => { if (!token) return fetch('/api/halal/bookmarks', { headers: { 'Authorization': `Bearer ${token}` } }) .then(r => r.json()) .then(d => { if (d.bookmarks) setBookmarked(new Set(d.bookmarks.map((b: any) => b.itemId))) }) .catch(() => {}) }, [token]) const fetchPlaces = useCallback(async (lat: number, lon: number) => { setLoading(true) setError(null) try { const res = await fetch(`/api/halal/places?lat=${lat}&lng=${lon}&radius=5000`, { headers: token ? { 'Authorization': `Bearer ${token}` } : {}, }) const data = await res.json() if (res.status === 429) { setError('Daily search limit reached. Upgrade to Premium for unlimited searches.') return } if (!res.ok) throw new Error(data.error || 'Failed to load places') setPlaces([...data.mosques, ...(data.restaurants || [])]) } catch (e: any) { setError(e.message || 'Failed to load places') } finally { setLoading(false) } }, [token]) const handleGeolocate = () => { if (!navigator.geolocation) { setError('Geolocation is not supported by your browser.') return } setLoading(true) navigator.geolocation.getCurrentPosition( pos => { const { latitude: lat, longitude: lon } = pos.coords setCenter({ lat, lon }) fetchPlaces(lat, lon) }, () => { setLoading(false) setError('Could not get your location. Try searching by city.') } ) } const handleCitySearch = async (e: React.FormEvent) => { e.preventDefault() if (!citySearch.trim()) return setLoading(true) setError(null) try { const res = await fetch( `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(citySearch)}&format=json&limit=1`, { headers: { 'User-Agent': 'FalahMobile/1.0' } } ) const results = await res.json() if (!results.length) { setError('City not found. Try a different search term.') setLoading(false) return } const lat = parseFloat(results[0].lat) const lon = parseFloat(results[0].lon) setCenter({ lat, lon }) fetchPlaces(lat, lon) } catch { setError('City search failed. Please try again.') setLoading(false) } } const handleBookmark = async (place: Place) => { if (!token) return const isBookmarked = bookmarked.has(place.id) if (isBookmarked) { const res = await fetch(`/api/halal/bookmarks?itemId=${place.id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` }, }) if (res.ok) setBookmarked(prev => { const next = new Set(prev); next.delete(place.id); return next }) } else { const res = await fetch('/api/halal/bookmarks', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ itemId: place.id, itemType: place.type, label: place.name }), }) if (res.ok) setBookmarked(prev => new Set([...prev, place.id])) } } if (authLoading) { return (
) } if (!token || !user) return if (!user.isPremium && !user.isPro) return const mosques = places.filter(p => p.type === 'mosque') const restaurants = places.filter(p => p.type === 'restaurant') return (
{/* Toolbar */}
Halal Monitor
setCitySearch(e.target.value)} className="w-full bg-gray-50 border border-gray-200 rounded-xl pl-8 pr-3 py-1.5 text-xs text-gray-900 placeholder-gray-400 focus:border-emerald-400 outline-none" />
{loading && } {places.length > 0 && ( {mosques.length} mosques · {restaurants.length} restaurants )}
{/* Error bar */} {error && (
{error}
)} {/* Map */}
{!center ? (

Search a city or use your location to find halal places.

) : leafletLoaded ? ( ) : null}
) } function MapWithMarkers({ center, places, bookmarked, onBookmark, }: { center: MapCenter places: Place[] bookmarked: Set onBookmark: (p: Place) => void }) { const [L, setL] = useState(null) useEffect(() => { import('leaflet').then(leaflet => { // Fix default marker icons for Next.js delete (leaflet.Icon.Default.prototype as any)._getIconUrl leaflet.Icon.Default.mergeOptions({ iconRetinaUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon-2x.png', iconUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', }) setL(leaflet) }) }, []) if (!L) return null const mosqueIcon = new L.Icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-green.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41], }) const restaurantIcon = new L.Icon({ iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-orange.png', shadowUrl: 'https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png', iconSize: [25, 41], iconAnchor: [12, 41], popupAnchor: [1, -34], shadowSize: [41, 41], }) return ( {places.map(place => (

{place.name}

{place.address &&

{place.address}

}

{place.type}

))}
) }