"use client"; import { useState, useEffect, useCallback, useMemo } from "react"; import { useAuth } from "@/lib/AuthContext"; import { useRouter } from "next/navigation"; import dynamic from "next/dynamic"; import { MapPin, Search, Heart, X, Star, ChevronRight, Sparkles, Crown, Loader2, Plus, Trash2, Send, Navigation, } from "lucide-react"; // Leaflet CSS — safe in client component import "leaflet/dist/leaflet.css"; // ─── Dynamic react-leaflet imports (SSR safe) ────────────── const LeafletMapContainer = 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 } ); // ─── Map click handler (for adding places) ───────────────── function MapClickHandler({ onMapClick }: { onMapClick: (lat: number, lng: number) => void }) { const { useMapEvents } = require("react-leaflet"); useMapEvents({ click: (e: any) => { onMapClick(e.latlng.lat, e.latlng.lng); }, }); return null; } // ─── Types ────────────────────────────────────────────────── interface Place { id: string; name: string; address: string; lat: number; lng: number; rating: number; type: "mosque" | "restaurant" | "cafe"; halal_certified: boolean; distance?: number; source?: string; } interface UsageInfo { queriesUsed: number; queriesLimit: number; remaining: number; tier: string; scope: string; } interface Bookmark { id: string; itemId: string; itemType: string; label: string | null; createdAt: string; place: Place | null; } interface UserLocation { lat: number; lng: number; } // ─── Haversine distance (km) ──────────────────────────────── function haversine( lat1: number, lng1: number, lat2: number, lng2: number ): number { const R = 6371; // Earth's radius in km const dLat = ((lat2 - lat1) * Math.PI) / 180; const dLng = ((lng2 - lng1) * Math.PI) / 180; const a = Math.sin(dLat / 2) ** 2 + Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) * Math.sin(dLng / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } // ─── Constants ────────────────────────────────────────────── const TYPES = ["all", "mosque", "restaurant", "cafe"] as const; const TYPE_LABELS: Record = { all: "All", mosque: "Mosques", restaurant: "Restaurants", cafe: "Cafes", }; const DEFAULT_CENTER: [number, number] = [3.139, 101.6869]; // Kuala Lumpur // ─── Page ─────────────────────────────────────────────────── export default function HalalMonitorPage() { const { user, token, loading } = useAuth(); const router = useRouter(); const [places, setPlaces] = useState([]); const [usage, setUsage] = useState(null); const [bookmarks, setBookmarks] = useState([]); const [search, setSearch] = useState(""); const [typeFilter, setTypeFilter] = useState("all"); const [selectedPlace, setSelectedPlace] = useState(null); const [pageLoading, setPageLoading] = useState(true); const [usageLoading, setUsageLoading] = useState(false); const [userLocation, setUserLocation] = useState(null); const [locationError, setLocationError] = useState(null); const [searchLocation, setSearchLocation] = useState(null); const [icons, setIcons] = useState | null>(null); const [userPlaces, setUserPlaces] = useState([]); const [showAddPlace, setShowAddPlace] = useState(false); const [addForm, setAddForm] = useState({ name: "", address: "", type: "restaurant", notes: "" }); const [mapClickPos, setMapClickPos] = useState<[number, number] | null>(null); const [submitting, setSubmitting] = useState(false); const isPremium = user?.isPremium || user?.isPro || false; const isFree = !isPremium; // ── Initialize Leaflet custom icons ─────────────────────── useEffect(() => { (async () => { const L = await import("leaflet"); // Fix default icon paths for bundlers delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ iconRetinaUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon-2x.png", iconUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-icon.png", shadowUrl: "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.9.4/images/marker-shadow.png", }); setIcons({ mosque: L.divIcon({ html: `
🕌
`, className: "", iconSize: [36, 36], iconAnchor: [18, 36], popupAnchor: [0, -36], }), restaurant: L.divIcon({ html: `
🍽️
`, className: "", iconSize: [36, 36], iconAnchor: [18, 36], popupAnchor: [0, -36], }), cafe: L.divIcon({ html: `
`, className: "", iconSize: [36, 36], iconAnchor: [18, 36], popupAnchor: [0, -36], }), user: L.divIcon({ html: `
`, className: "", iconSize: [20, 20], iconAnchor: [10, 10], }), userPlace: L.divIcon({ html: `
`, className: "", iconSize: [36, 36], iconAnchor: [18, 36], popupAnchor: [0, -36], }), }); })(); }, []); // ── GeoLocation ─────────────────────────────────────────── useEffect(() => { if (!navigator.geolocation) { setLocationError("Geolocation is not supported by your browser"); return; } navigator.geolocation.getCurrentPosition( (pos) => { setUserLocation({ lat: pos.coords.latitude, lng: pos.coords.longitude, }); setLocationError(null); }, (err) => { setLocationError(err.message); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 300000 } ); }, []); // ── Redirect if not logged in ───────────────────────────── useEffect(() => { if (!loading && !token) router.push("/auth"); }, [loading, token, router]); // ── Fetch places ────────────────────────────────────────── const fetchPlaces = useCallback(async (q = search, type = typeFilter) => { try { const params = new URLSearchParams(); if (q) params.set("q", q); if (type && type !== "all") params.set("type", type); if (userLocation) { params.set("lat", userLocation.lat.toString()); params.set("lng", userLocation.lng.toString()); } const res = await fetch(`/mobile/api/halal/places?${params.toString()}`); if (res.ok) { const data = await res.json(); setPlaces(data.places); if (data.searchLocation) setSearchLocation(data.searchLocation); } } catch { // ignore } }, [search, typeFilter, userLocation]); // ── Fetch usage ─────────────────────────────────────────── const fetchUsage = useCallback(async () => { if (!token) return; try { const res = await fetch("/mobile/api/halal/usage", { headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { const data = await res.json(); setUsage(data); } } catch { // ignore } }, [token]); // ── Fetch bookmarks ─────────────────────────────────────── const fetchBookmarks = useCallback(async () => { if (!token) return; try { const res = await fetch("/mobile/api/halal/bookmarks", { headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { const data = await res.json(); setBookmarks(data.bookmarks); } } catch { // ignore } }, [token]); // ── Fetch user-contributed places ───────────────────────── const fetchUserPlaces = useCallback(async () => { try { const res = await fetch("/mobile/api/halal/user-places"); if (res.ok) { const data = await res.json(); setUserPlaces(data.places.map((p: any) => ({ id: `user-${p.id}`, name: p.name, address: p.address, lat: p.lat, lng: p.lng, rating: 4.0, type: p.type, halal_certified: true, source: "user", notes: p.notes, userId: p.user?.id, userName: p.user?.name, createdAt: p.createdAt, }))); } } catch { // ignore } }, []); // ── Submit a new user place ───────────────────────────── const handleAddPlace = useCallback(async () => { if (!token || !addForm.name.trim() || !mapClickPos) return; setSubmitting(true); try { const res = await fetch("/mobile/api/halal/user-places", { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ name: addForm.name, address: addForm.address, lat: mapClickPos[0], lng: mapClickPos[1], type: addForm.type, notes: addForm.notes, }), }); if (res.ok) { setShowAddPlace(false); setAddForm({ name: "", address: "", type: "restaurant", notes: "" }); setMapClickPos(null); await fetchUserPlaces(); } } catch { // ignore } finally { setSubmitting(false); } }, [token, addForm, mapClickPos, fetchUserPlaces]); // ── Delete a user place ───────────────────────────────── const handleDeletePlace = useCallback(async (placeId: string) => { if (!token) return; const realId = placeId.replace("user-", ""); try { await fetch(`/mobile/api/halal/user-places?id=${realId}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, }); await fetchUserPlaces(); } catch { // ignore } }, [token, fetchUserPlaces]); // ── Track usage query ───────────────────────────────────── const trackQuery = useCallback(async () => { if (!token) return; setUsageLoading(true); try { const res = await fetch("/mobile/api/halal/usage", { method: "POST", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }); if (res.ok) { const data = await res.json(); setUsage((prev) => (prev ? { ...prev, ...data } : data)); } else if (res.status === 429) { setUsage((prev) => (prev ? { ...prev, remaining: 0 } : prev)); } } catch { // ignore } finally { setUsageLoading(false); } }, [token]); // ── Initial load ────────────────────────────────────────── useEffect(() => { if (loading || !token) return; const init = async () => { setPageLoading(true); await Promise.all([fetchPlaces(), fetchUsage(), fetchBookmarks(), fetchUserPlaces()]); setPageLoading(false); }; init(); }, [loading, token, fetchPlaces, fetchUsage, fetchBookmarks]); // ── Re-fetch when geolocation arrives (if initial load beat it) ── useEffect(() => { if (userLocation && !pageLoading) { fetchPlaces(); } // Only fire when userLocation transitions from null → value // eslint-disable-next-line react-hooks/exhaustive-deps }, [userLocation]); // ── Handle map click for adding places ──────────────────── const handleMapClick = useCallback((lat: number, lng: number) => { setMapClickPos([lat, lng]); if (!showAddPlace) { setShowAddPlace(true); } }, [showAddPlace]); // ── Compute distances & sort by distance ────────────────── const sortedPlaces = useMemo(() => { const allPlaces = [...places, ...userPlaces]; if (!allPlaces.length) return []; const withDistances = allPlaces.map((p) => { if (userLocation) { const dist = haversine( userLocation.lat, userLocation.lng, p.lat, p.lng ); return { ...p, distance: Math.round(dist * 100) / 100 }; } return p; }); if (userLocation) { return [...withDistances].sort( (a, b) => (a.distance ?? Infinity) - (b.distance ?? Infinity) ); } return withDistances; }, [places, userPlaces, userLocation]); // Free tier shows ALL place types (rate-limiting stays on query tracking) const visiblePlaces = sortedPlaces; // ── Search / filter triggers ────────────────────────────── const handleSearch = useCallback(() => { fetchPlaces(search, typeFilter); if (!isFree) trackQuery(); }, [search, typeFilter, fetchPlaces, isFree, trackQuery]); const handleTypeFilter = useCallback( (type: string) => { setTypeFilter(type); fetchPlaces(search, type); if (!isFree) trackQuery(); }, [search, fetchPlaces, isFree, trackQuery] ); // ── Bookmark toggle ─────────────────────────────────────── const toggleBookmark = useCallback( async (place: Place) => { if (!token) return; const existing = bookmarks.find((b) => b.itemId === place.id); if (existing) { const res = await fetch( `/mobile/api/halal/bookmarks?id=${existing.id}`, { method: "DELETE", headers: { Authorization: `Bearer ${token}` }, } ); if (res.ok) fetchBookmarks(); } else { const res = await fetch("/mobile/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) fetchBookmarks(); } }, [token, bookmarks, fetchBookmarks] ); const isBookmarked = useCallback( (placeId: string) => bookmarks.some((b) => b.itemId === placeId), [bookmarks] ); // ── Render stars ────────────────────────────────────────── const renderStars = (rating: number) => { const full = Math.floor(rating); const half = rating - full >= 0.5; const empty = 5 - full - (half ? 1 : 0); return ( {Array.from({ length: full }, (_, i) => ( ))} {half && (
)} {Array.from({ length: empty }, (_, i) => ( ))} {rating.toFixed(1)}
); }; // ── Map center ──────────────────────────────────────────── const mapCenter: [number, number] = userLocation ? [userLocation.lat, userLocation.lng] : DEFAULT_CENTER; // ── Premium Gating Overlay ───────────────────────────────── if (loading) { return (
); } if (!user) return null; return (
{/* ── Header ───────────────────────────────────────── */}

Halal Monitor

{usage && (

Today

{usage.remaining}/{usage.queriesLimit}

)}

Discover halal-certified places around you

{/* ── Premium Upgrade Banner ─────────────────────── */} {isFree && ( )} {/* Free user scope notice */} {isFree && (
Free tier: 10 queries/day
)}
{/* ── Search Bar ───────────────────────────────────── */}
setSearch(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} className="flex-1 bg-transparent text-sm text-white placeholder-gray-600 outline-none" /> {search && ( )}
{/* ── Category Filter ──────────────────────────────── */}
{TYPES.map((t) => { const active = typeFilter === t; // All filter buttons enabled for free tier (rate limiting stays) const isDisabled = false; return ( ); })}
{/* ── Leaflet Map ──────────────────────────────────── */}
{/* User location blue dot */} {icons && userLocation && ( You are here )} {/* Place markers */} {icons && visiblePlaces.map((place) => ( setSelectedPlace(place), }} >
{place.name}
{place.address}
))}
{/* ── Results List ─────────────────────────────────── */}

{visiblePlaces.length}{" "} {visiblePlaces.length === 1 ? "place" : "places"} {userLocation && " · Nearest first"}

{searchLocation && (

{searchLocation}

)}
{usageLoading && ( )}
{pageLoading ? (
) : visiblePlaces.length === 0 && locationError ? (

Location unavailable

Enable location or type a city name above.

) : visiblePlaces.length === 0 && !pageLoading ? (

No halal places found nearby

Try a different area or search for a city.

) : (
{visiblePlaces.map((place) => { const bookmarked = isBookmarked(place.id); return (
setSelectedPlace(place)} className="bg-[#111118] border border-gray-800/60 rounded-2xl p-4 active:scale-[0.99] transition cursor-pointer" >
{/* Place name + type badge */}

{place.name}

{place.type === "mosque" ? "Mosque" : place.type === "restaurant" ? "Restaurant" : "Cafe"}
{/* Address */}

{place.address}

{/* Rating */}
{renderStars(place.rating)}
{/* Distance & Halal badge */}
{place.distance !== undefined && ( {place.distance < 1 ? `${(place.distance * 1000).toFixed(0)} m` : `${place.distance.toFixed(1)} km`} )} {place.halal_certified && ( Halal Certified )}
{/* Bookmark heart */}
); })}
)}
{/* ── Detail Modal ──────────────────────────────────── */} {selectedPlace && (
setSelectedPlace(null)} >
e.stopPropagation()} className="bg-[#111118] border border-gray-800/60 rounded-t-3xl sm:rounded-3xl w-full max-w-md max-h-[85vh] overflow-y-auto animate-fade-in" > {/* Handle */}
{/* Close button */}
{/* Type badge */}
{selectedPlace.type === "mosque" ? "🕌 Mosque" : selectedPlace.type === "restaurant" ? "🍽️ Restaurant" : "☕ Cafe"}
{/* Name */}

{selectedPlace.name}

{/* Halal certified */} {selectedPlace.halal_certified && (
Halal Certified
)} {/* Address */}

{selectedPlace.address}

{/* Rating */}
{selectedPlace.rating.toFixed(1)} / 5.0
{/* Distance (if available) */} {selectedPlace.distance !== undefined && (
{selectedPlace.distance < 1 ? `${(selectedPlace.distance * 1000).toFixed(0)} m` : `${selectedPlace.distance.toFixed(1)} km`}{" "} from your location
)} {/* Coords */}

Latitude

{selectedPlace.lat.toFixed(4)}

Longitude

{selectedPlace.lng.toFixed(4)}

{/* Open in Google Maps */} Open in Google Maps {/* Bookmark button */} {/* Delete button for user-contributed places */} {selectedPlace.source === "user" && ( )}
)} {/* ── Add Place Modal ────────────────────────────────── */} {showAddPlace && (
setShowAddPlace(false)} >
e.stopPropagation()} className="bg-[#111118] border border-gray-800/60 rounded-t-3xl sm:rounded-3xl w-full max-w-md max-h-[85vh] overflow-y-auto animate-fade-in" >
{/* Close button */}

Add a Place

Share a halal place you know. Yellow markers help the community.

{/* Map click hint */} {!mapClickPos && (

👆 Tap on the map to set a location, then fill in the details

)} {mapClickPos && (

📍 Location set

{mapClickPos[0].toFixed(4)}, {mapClickPos[1].toFixed(4)}

)} {/* Name */} {/* Type selector */} {/* Address */} {/* Notes */}