'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.
- ✓ Mosques and prayer spaces worldwide
- ✓ Halal restaurants near you
- ✓ Bookmark favourite places
- ✓ City search anywhere in the world
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
{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}
))}
)
}